board-game
Version:
an online board game engine
901 lines (870 loc) • 35.5 kB
JavaScript
import 'babel-polyfill';
import $ from 'jquery';
import Vue from 'vue';
import io from 'socket.io-client';
import utilTools from './utils';
import qrcode from 'qrcode';
const keyNames = [
'', '', '', '', '', '', '', '', '', '', //000-009
'', '', '', 'enter', '', '', 'shift', 'ctrl', 'alt', '', //010-019
'', '', '', '', '', '', '', '', '', '', //020-029
'', '', 'space', '', '', '', '', 'left', 'up', 'right', //030-039
'down', '', '', '', '', '', '', '', '', '', //040-049
'', '', '', '', '', '', '', '', '', '', //050-059
'', '', '', '', '', 'a', 'b', 'c', 'd', 'e', //060-069
'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', //070-079
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', //080-089
'z', '', '', '', '', '', '', '', '', '', //090-099
'', '', '', '', '', '', '', '', '', '', //100-109
'', '', '', '', '', '', '', '', '', '', //110-119
'', '', '', '', '', '', '', '', '', '', //120-129
'', '', '', '', '', '', '', '', '', '', //130-139
'', '', '', '', '', '', '', '', '', '', //140-149
'', '', '', '', '', '', '', '', '', '', //150-159
'', '', '', '', '', '', '', '', '', '', //160-169
'', '', '', '', '', '', '', '', '', '', //170-179
'', '', '', '', '', '', '', '', '', '', //180-189
'', '', '', '', '', '', '', '', '', '', //190-199
];
const keyStates = keyNames.map(v => false);
const getEventName = (type, name) => `${ type }.${ name }`;
const clone = obj => {
return JSON.parse(JSON.stringify(obj));
};
const clear = value => {
if (typeof value === 'object' && value !== null) {
for (let [k, v] of Object.entries(value)) {
if (!k.startsWith('~')) {
value[k] = clear(v);
}
}
for (let [k, v] of Object.entries(value)) {
if (k.startsWith('~') && v === true) {
delete value[k];
}
}
}
return value;
};
const setProp = (obj, prop, value) => {
if (Array.isArray(obj)) {
obj.splice(prop, 1, clear(value));
} else {
Vue.set(obj, prop, clear(value));
}
};
const set = (obj, prop, value) => {
if (typeof obj === 'undefined') return;
if (prop.startsWith('~')) {
if (value === true) {
Vue.delete(obj, prop.substring(1));
}
} else {
let old = obj[prop];
if (typeof old === 'undefined') {
setProp(obj, prop, value);
} else {
if (old !== value) {
if (typeof value === 'object' && value !== null) {
if (typeof old !== 'object') {
setProp(obj, prop, value);
} else {
if (Array.isArray(old) && Array.isArray(value)) {
old.splice(value.length);
}
for (let name in value) {
if (value.hasOwnProperty(name)) {
set(old, name, value[name]);
}
}
}
} else {
setProp(obj, prop, value);
}
}
}
}
};
const append = (array, items) => {
items.forEach(item => {
if (item === '~') {
array.splice(array.length - 1, 1);
} else if (item === '*') {
array.splice(0);
} else {
array.push(item);
}
});
};
const getTouch = (event, target, index) => {
let filter = touch => $(target).closest(touch.target).length > 0 || $(touch.target).closest(target).length > 0;
if (event.targetTouches) {
let touches = [...event.targetTouches].filter(filter);
if (typeof index === 'function') {
let touch = touches.find(index);
if (touch) return touch;
} else if (touches.length > index) {
return touches[index];
}
}
if (event.changedTouches) {
let touches = [...event.changedTouches].filter(filter);
if (typeof index === 'function') {
let touch = touches.find(index);
if (touch) return touch;
} else if (touches.length > index) {
return touches[index];
}
}
return event;
};
const audioContext = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext)();
// const Base64Binary = {
// _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
//
// /* will return a Uint8Array type */
// decodeArrayBuffer: function(input) {
// let bytes = (input.length/4) * 3;
// let ab = new ArrayBuffer(bytes);
// this.decode(input, ab);
//
// return ab;
// },
//
// removePaddingChars: function(input){
// let lkey = this._keyStr.indexOf(input.charAt(input.length - 1));
// if(lkey === 64){
// return input.substring(0,input.length - 1);
// }
// return input;
// },
//
// decode: function (input, arrayBuffer) {
// //get last chars to see if are valid
// input = this.removePaddingChars(input);
// input = this.removePaddingChars(input);
//
// let bytes = parseInt((input.length / 4) * 3, 10);
//
// let uarray;
// let chr1, chr2, chr3;
// let enc1, enc2, enc3, enc4;
// let i = 0;
// let j = 0;
//
// if (arrayBuffer)
// uarray = new Uint8Array(arrayBuffer);
// else
// uarray = new Uint8Array(bytes);
//
// input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
//
// for (i=0; i<bytes; i+=3) {
// //get the 3 octects in 4 ascii chars
// enc1 = this._keyStr.indexOf(input.charAt(j++));
// enc2 = this._keyStr.indexOf(input.charAt(j++));
// enc3 = this._keyStr.indexOf(input.charAt(j++));
// enc4 = this._keyStr.indexOf(input.charAt(j++));
//
// chr1 = (enc1 << 2) | (enc2 >> 4);
// chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
// chr3 = ((enc3 & 3) << 6) | enc4;
//
// uarray[i] = chr1;
// if (enc3 !== 64) uarray[i+1] = chr2;
// if (enc4 !== 64) uarray[i+2] = chr3;
// }
//
// return uarray;
// }
// }
const audioMap = new Map();
class AudioPlayer {
constructor(mp3, ogg) {
if (audioMap.has(mp3)) {
this.audioBuffer = audioMap.get(mp3);
} else {
let load = (url, onerror) => {
let request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = () => {
return audioContext.decodeAudioData(request.response, buffer => {
audioMap.set(mp3, buffer);
this.audioBuffer = buffer;
}, e => { onerror && onerror(e) });
};
request.onerror = e => { onerror && onerror(e) };
request.send();
};
load(mp3, e => ogg ? load(ogg, e => console.log(e)) : console.log(e));
}
this.complete = [];
this.paused = true;
this.startTime = 0;
this.volume = 1;
this.timer = false;
}
get duration() {
return this.audioBuffer ? this.audioBuffer.duration : 0;
}
get currentTime() {
return this.startTime === 0 ? 0 : ((Date.now() - this.startTime) / 1000);
}
pause() {
if (this.audioSource && !this.paused) {
this.audioSource.stop();
this.paused = true;
if (this.timer) {
clearTimeout(this.timer);
this.timer = false;
}
}
}
play(second) {
if (!this.audioBuffer) return;
if (typeof second === 'undefined' && !this.paused) return;
second = second || 0;
this.pause();
let audioSource = audioContext.createBufferSource();
audioSource.buffer = this.audioBuffer;
if (this.volume === 1) {
audioSource.connect(audioContext.destination);
} else {
let gainNode = audioContext.createGain();
gainNode.gain.value = this.volume;
audioSource.connect(gainNode);
gainNode.connect(audioContext.destination);
}
this.audioSource = audioSource;
this.audioSource.start(0, second);
this.paused = false;
this.startTime = Date.now() - Math.floor(second * 1000);
if (this.duration !== 0) {
this.timer = setTimeout(() => {
this.complete.forEach(complete => complete());
this.paused = true;
}, Math.ceil((this.duration - second) * 1000));
}
}
onComplete(callback) {
this.complete.push(callback);
}
offComplete(callback) {
this.complete = callback ? this.complete.filter(c => c !== callback) : [];
}
}
const utils = {
getAudio(mp3, ogg) {
return new AudioPlayer(mp3, ogg);
},
get touchable() {
return navigator.maxTouchPoints > 0 || /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent);
},
get vw() {
return parseInt($(window).width());
},
get vh() {
return parseInt($(window).height());
},
get vmin() {
return Math.min(this.vw, this.vh);
},
get vmax() {
return Math.max(this.vw, this.vh);
},
isPortrait() {
return this.vw < this.vh;
},
isLandscape() {
return this.vw >= this.vh;
},
point(event, target, index = 0) {
if (!isNaN(target)) {
index = target;
target = $(event.target);
} else {
target = $(target || event.target);
}
let touch = getTouch(event, target, index);
let result = {
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
identifier: touch.identifier || 0
};
let offset = target.offset();
result.x = result.pageX - offset.left;
result.y = result.pageY - offset.top;
return result;
},
units: {
rad: 'rad',
turn: 'turn',
deg: 'deg',
grad: 'grad',
px: 'px',
pt: 'pt',
in: 'in',
cm: 'cm',
mm: 'mm',
vw: 'vw',
vh: 'vh',
vmin: 'vmin',
vmax: 'vmax',
s: 's',
ms: 'ms'
},
asPx(value, unit = 'px') {
unit = (/(px|pt|in|cm|mm|vw|vh|vmin|vmax)$/i.exec(value) || [ unit ])[0];
value = parseFloat(value);
switch (unit.toLowerCase()) {
case 'px': return value;
case 'pt': return value * 4 / 3;
case 'in': return value * 96;
case 'cm': return value * 48 / 1.27;
case 'mm': return value * 48 / 12.7;
case 'vw': return value * this.vw / 100;
case 'vh': return value * this.vh / 100;
case 'vmin': return value * this.vmin / 100;
case 'vmax': return value * this.vmax / 100;
}
},
asPt(value, unit = 'px') {
return this.asPx(value, unit) * 3 / 4;
},
asIn(value, unit = 'px') {
return this.asPx(value, unit) / 96;
},
asCm(value, unit = 'px') {
return this.asPx(value, unit) * 1.27 / 48;
},
asMm(value, unit = 'px') {
return this.asPx(value, unit) * 12.7 / 48;
},
asVm(value, unit = 'px') {
return this.asPx(value, unit) * 100 / this.vm;
},
asVh(value, unit = 'px') {
return this.asPx(value, unit) * 100 / this.vh;
},
asVmin(value, unit = 'px') {
return this.asPx(value, unit) * 100 / this.vmin;
},
asVmax(value, unit = 'px') {
return this.asPx(value, unit) * 100 / this.vmax;
},
asSecond(value, unit = 's') {
unit = (/(s|ms)$/i.exec(value) || [ unit ])[0];
value = parseFloat(value);
switch (unit.toLowerCase()) {
case 's' : return value;
case 'ms' : return value / 1000;
}
},
asMillisecond(value, unit = 's') {
return this.asSecond(value, unit) * 1000;
},
asRad(value, unit = 'rad') {
unit = (/(deg|turn|grad|rad)$/i.exec(value) || [ unit ])[0];
value = parseFloat(value);
switch (unit.toLowerCase()) {
case 'rad': return value;
case 'turn': return value * (2 * Math.PI);
case 'deg': return value / 180 * Math.PI;
case 'grad': return value / 200 * Math.PI;
}
return value;
},
asDeg(value, unit = 'rad') {
return this.asRad(value, unit) / Math.PI * 180;
},
asTurn(value, unit = 'rad') {
return this.asRad(value, unit) / (Math.PI * 2);
},
asGrad(value, unit = 'rad') {
return this.asRad(value, unit) / Math.PI * 200;
},
...utilTools
};
const mixin = adapter && adapter.mixin || { };
const getKeyEventNames = type => {
let result = { };
for (let name of keyNames) {
if (name) result[name] = getEventName(type, name);
}
return new Proxy(result, {
get(target, property, receiver) {
return Reflect.get(target, property, receiver) || `${ type }.${ property }`;
}
});
};
const getControlEventNames = type => {
let result = { };
for (let name of [ 'a', 'b', 'x', 'y' ]) {
if (name) result[name] = getEventName(type, name);
}
return new Proxy(result, {
get(target, property, receiver) {
return Reflect.get(target, property, receiver) || `${ type }.${ property }`;
}
});
};
const events = {
key: {
start: getKeyEventNames('key.start'),
end: getKeyEventNames('key.end'),
press: getKeyEventNames('key.press'),
},
control: {
start: getControlEventNames('control.start'),
end: getControlEventNames('control.end'),
press: getControlEventNames('control.press')
}
};
export { mixin, utils, events, Vue };
export default (application, listeners, callback) => {
const inBrowser = typeof window !== 'undefined';
const UA = inBrowser && window.navigator.userAgent.toLowerCase();
const isMobile = (UA && /iphone|ipad|ipod|ios|android/.test(UA));//是否为手机环境
if (config.dev) {
if (isMobile) {
require(['@whinc/web-console'], WebConsole => {
new WebConsole(config.webconsole || {});
});
} else {
Vue.config.devtools = config.devtools;
}
}
try {
document.domain = (/(\d{1,2}|[01]\d{2}|2[0-4]\d{2}|25[0-5])(\.(\d{1,2}|[01]\d{2}|2[0-4]\d{2}|25[0-5])){3}/ig.test(location.hostname) ? location.hostname : (location.hostname.match(/[^.]+\.[^.]+$/ig) || [location.hostname])[0]);
} catch (e) { }
document.title = config.name;
$(() => {
let fullScreenTimer;
function listenCallBack(){
if(isMobile && utils.isLandscape()) {
$('#scrollTipMask').css({ display: 'block' });
$('html, body').css({ overflow: 'auto', 'touch-action': 'auto' });
if( window.innerHeight === document.documentElement.clientHeight ){ //全屏并且加载完毕,则不显示div
$('#scrollTipMask').css({ display: 'none' });
$('html, body').css({ overflow: 'hidden', 'touch-action': 'none' });
} else { //未全屏显示则把div显示出来
$('html, body').css({ overflow: 'auto', 'touch-action': 'auto' });
clearTimeout(fullScreenTimer);
fullScreenTimer = setTimeout(() => $(document).scrollTop(0), 40);
}
}
}
$(window).on('resize', listenCallBack).on('scroll', listenCallBack);
listenCallBack();
$(document).on('gesturestart', e => {
if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('dblclick', e => {
if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('touchmove', e => {
if (!$(e.target).parents('.modal,.scrollable,.web-console,#scrollTipMask').length && e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('touchstart', e => {
if (!$(e.target).parents('.modal,.scrollable,.web-console,#scrollTipMask').length && !$(e.target).is('input,select,textarea') && e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('touchend', e => {
if (!$(e.target).parents('.modal,.scrollable,.web-console,#scrollTipMask').length && !$(e.target).is('input,select,textarea') && e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('hold', e => {
if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('contextmenu', e => {
if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
}).on('selectstart', e => {
if (e.cancelable && !e.isDefaultPrevented()) e.preventDefault();
});
const storage = new Proxy({ }, {
get(target, property, receiver) {
if (property in target || typeof property === 'symbol') {
return Reflect.get(target, property, receiver);
} else {
let value = localStorage.getItem(property);
return value && JSON.parse(value);
}
},
set(target, property, value, receiver) {
if (property in target || typeof property === 'symbol') {
Reflect.set(target, property, value, receiver);
} else {
localStorage.setItem(property, JSON.stringify(value));
}
return true;
}
});
const lazy = {
queue: [],
timer: false,
exec(executor) {
if (typeof executor !== 'undefined') {
this.queue.push(executor);
}
if (this.queue.length) {
clearTimeout(this.timer);
this.timer = setTimeout(() => this.execute(), 0);
}
},
execute() {
while (this.queue.length) {
this.queue.shift().call();
}
}
};
const socket = new Proxy(io(location.protocol + '//' + location.hostname + ':' + location.port, {
reconnection: false,
transports: ['websocket', 'polling']
}), {
get(target, property, receiver) {
if (property === 'on') {
let on = Reflect.get(target, property, receiver);
return arg => {
if (typeof arg === 'object' && arg !== null) {
for (let key in arg) {
if (arg.hasOwnProperty(key)) {
on.call(target, key, data => lazy.exec(arg[key].bind(target, data)));
}
}
} else {
on.apply(target, arguments);
}
}
} else {
return Reflect.get(target, property, receiver);
}
}
});
const queue = {
queue: [],
timer: false,
exec(executor) {
if (typeof executor !== 'undefined') {
this.queue.push(executor);
}
this.doExecute();
},
doExecute() {
clearTimeout(this.timer);
if (this.queue.length) {
this.timer = setTimeout(() => {
this.queue.shift().call();
this.doExecute();
}, 0);
}
}
};
const emit = new Proxy({ }, {
get(target, property, receiver) {
if (property in target) {
return Reflect.get(target, property, receiver);
} else {
return data => queue.exec(() => socket.emit(property.replace(/^[_$]/i, ''), data));
}
}
});
const game = {
data: {
loading: true,
player: { },
rooms: {
loading: false,
page: 0,
list: [],
status: {
waiting: '等待中',
gaming: '游戏中',
sorting: '准备中'
}
},
dialog: {
alert: false,
settings: false,
join: false,
player: false,
login: false
},
tip: {
more: false
},
modal: {
introduction: false,
qr: false
},
config: clone(config)
},
computed: {
qr: function() {
let url = window.location.origin + window.location.pathname + (this.player && this.player.room ? '#' + this.player.room.id : '');
let qr = qrcode(0, 'L');
qr.addData(url);
qr.make();
return qr.createDataURL(10, 15);
// return 'http://pan.baidu.com/share/qrcode?w=150&h=150&url=' + encodeURIComponent(window.location.origin + window.location.pathname + '#' + this.player.room.id)
},
dialogs: function() {
return Object.values(this.dialog).some(dialog => !!dialog);
}
},
mounted() {
if (this.config.login) {
this.loading = false;
this.dialog.login = {
id: storage.player && storage.player.id || '', password: ''
}
} else {
this.loading = true;
if (storage.player) {
this.player = storage.player;
setTimeout(() => {
emit.login(this.player);
}, 0);
} else {
$.get(['id']).then(data => {
this.player.id = data;
emit.login(this.player);
}).catch(() => {
setTimeout(() => window.location.reload(), 500);
});
}
}
this.getRooms();
},
methods: {
showControl() {
this.$control().show();
},
hideControl() {
this.$control().hide();
},
random(callback) {
let name = this.dialog.player.name;
$.get(['random']).then(data => {
if (name === this.dialog.player.name) {
this.dialog.player.name = data;
callback && typeof callback === 'function' && callback();
}
});
},
doLogin() {
if (!this.dialog.login.id) this.alert('请输入用户名');
else if (!this.dialog.login.password) this.alert('请输入密码');
else emit.login(this.player = this.dialog.login);
},
help() {
this.modal.introduction = true;
},
mine() {
this.dialog.player = { id: this.player.id, name: this.player.name }
},
share() {
this.modal.qr = true;
},
settings() {
this.dialog.settings = this.room.settings;
},
more() {
this.tip.more = !this.tip.more;
},
home() {
window.location.href = this.config.home;
},
getRooms: function() {
if (!this.rooms.loading) {
this.rooms.loading = true;
emit.rooms();
}
},
alert(message) {
this.dialog.alert = message;
},
prepare() {
this.loading = true;
emit.settings();
},
create() {
this.loading = true;
emit.create(this.dialog.settings);
this.dialog.settings = false;
},
preJoin(roomId) {
this.dialog.join = { roomId: roomId && !isNaN(roomId) ? roomId : '' };
},
join() {
if (this.dialog.join && this.dialog.join.roomId && !isNaN(this.dialog.join.roomId)) {
this.loading = true;
emit.join(this.dialog.join);
this.dialog.join = false;
}
},
joinTo(roomId) {
this.preJoin(roomId);
this.join();
},
exit() {
this.loading = true;
emit.exit();
},
start() {
if (this.player.room.canStart && this.player.room.creator.id === this.player.id) {
this.loading = true;
emit.start();
}
},
move(role, team, index) {
let data = { team, index };
if (role === true) {
this.loading = true;
if (this.creator) {
emit.disable(data);
} else {
emit.move(data);
}
} else if (role === false) {
if (this.creator) {
this.loading = true;
emit.enable(data);
}
} else if (role.player.id !== this.player.id) {
if (this.creator) {
if(confirm('是否踢出玩家 ' + role.player.name)) {
this.loading = true;
emit.kick(data);
}
}
}
},
confirm() {
this.loading = true;
emit.confirm();
},
savePlayer() {
this.loading = true;
if (!this.dialog.player.name) {
this.random(() => this.savePlayer());
} else {
emit.info(this.dialog.player);
this.dialog.player = false;
}
},
onControl(control) {
this.$emit('control', control);
},
$game: function() {
return this.$refs.game;
},
$control: function() {
return this.$refs.control;
}
}
};
if (!application) application = () => ({ });
if ($.isFunction(application)) application = application({ socket, emit, storage, set, append }) || { };
if (!adapter || !adapter.create) throw '无法初始化';
const app = adapter.create(application, game);
app.socket = socket;
app.emit = emit;
app.storage = storage;
socket.on({
player(player) {
set(app, 'player', player);
let roomId = parseInt((window.location.hash || '').replace(/^.*?#/ig, '').replace(/[^\d].+/ig, ''));
if (roomId && !isNaN(roomId)) {
if (app.room) {
window.location.href = window.location.origin + window.location.pathname;
} else {
app.joinTo(roomId.toString());
}
}
if (!app.room) app.getRooms();
app.loading = false;
storage.player = { id: app.player.id, name: app.player.name };
app.dialog.login = false;
if (!app.player.name) {
app.dialog.player = { id: app.player.id, name: app.player.name };
}
},
rooms(rooms) {
app.rooms.list = rooms.page === 0 ? rooms.list : app.rooms.list.concat(rooms.list);
app.rooms.page = rooms.page;
app.rooms.loading = false;
if (app.rooms.list.length === 0) {
setTimeout(() => app.getRooms(), 1000);
}
},
role(role) {
set(app.player, 'role', role);
app.loading = false;
},
room(room) {
set(app.player, 'room', room);
if (app.game) emit.ready();
app.loading = false;
},
game(game) {
app.room && set(app.room, 'game', game);
app.loading = false;
},
group(summary) {
app.game && app.game.groups && set(app.game.groups, summary.index.toString(), summary);
app.loading = false;
},
board(board) {
app.game && set(app.game, 'board', board);
},
message(message) {
if (app.game) {
append(app.game.messages, message);
} else if (app.room) {
append(app.room.messages, message);
}
},
settings(settings) {
app.dialog.settings = settings;
app.loading = false;
},
err(err) {
err ? app ? app.alert(err) : alert(err) : window.location.reload();
app && (app.loading = false);
},
disconnect() {
app.loading = true;
let retryTimes = 0;
let retry = function() {
retryTimes++;
$.get(['id']).then(function() {
window.location.reload();
}).catch(function() {
setTimeout(retry, Math.min(retryTimes, 10) * 500);
});
};
retry();
}
});
if (!listeners) listeners = () => ({ });
if ($.isFunction(listeners)) listeners = listeners({ socket, emit, app, storage, set, append }) || { };
socket.on(listeners);
emit.connection();
window.alert = function(message) {
if (app.view) {
app.view.alert = message;
} else if (app.dialog) {
app.dialog.alert = message;
}
};
$(document).on('keypress', event => app.$emit('key.press', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey }));
$(document).on('keydown', event => {
app.$emit('key.start.repeat', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey });
if (!keyStates[event.keyCode]) {
keyStates[event.keyCode] = true;
app.$emit('key.start', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey });
}
});
$(document).on('keyup', event => {
keyStates[event.keyCode] = false;
app.$emit('key.end', { code: event.keyCode, name: keyNames[event.keyCode] || event.key || '', shift: event.shiftKey, alt: event.altKey, ctrl: event.ctrlKey });
});
$(window).on('resize', event => app.$emit('resize'));
callback && callback();
});
};