// // virtualKeyboard.js // examples // // Created by Thijs Wenker on 11/18/14. // Copyright 2014 High Fidelity, Inc. // // Control a virtual keyboard using your favorite HMD. // Usage: Enable VR-mode and go to First person mode, // look at the key that you would like to press, and press the spacebar on your "REAL" keyboard. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // experimental 3dmode THREE_D_MODE = false; KBD_UPPERCASE_DEFAULT = 0; KBD_LOWERCASE_DEFAULT = 1; KBD_UPPERCASE_HOVER = 2; KBD_LOWERCASE_HOVER = 3; KBD_BACKGROUND = 4; KEYBOARD_URL = Script.getExternalPath(Script.ExternalPaths.Assets, "images/keyboard.svg"); CURSOR_URL = Script.getExternalPath(Script.ExternalPaths.Assets, "images/cursor.svg"); RETURN_CHARCODE = 0x01000004; ENTER_CHARCODE = 0x01000005; SPACEBAR_CHARCODE = 32; KEYBOARD_WIDTH = 1174.7; KEYBOARD_HEIGHT = 434.1; CURSOR_WIDTH = 33.9; CURSOR_HEIGHT = 33.9; KEYBOARD_SCALE_MULTIPLIER = 0.50; // VIEW_ANGLE can be adjusted to your likings, the smaller the faster movement. // Try setting it to 60 if it goes too fast for you. const VIEW_ANGLE = 40.0; const VIEW_ANGLE_BY_TWO = VIEW_ANGLE / 2; const BOUND_X = 0; const BOUND_Y = 1; const BOUND_W = 2; const BOUND_H = 3; KEY_STATE_LOWER = 0; KEY_STATE_UPPER = 1; var windowDimensions = Controller.getViewportDimensions(); KeyboardKey = (function(keyboard, keyProperties) { var tthis = this; this._focus = false; this._beingPressed = false; this.event = keyProperties.event != undefined ? keyProperties.event : 'keypress'; this.bounds = keyProperties.bounds; this.states = keyProperties.states; this.keyboard = keyboard; this.keyState = keyProperties.keyState != undefined ? keyProperties.keyState : KBD_LOWERCASE_DEFAULT; // one overlay per bound vector [this.bounds] this.overlays = []; this.getKeyEvent = function() { if (tthis.event == 'keypress') { var state = tthis.states[(tthis.keyboard.shift ? 1 : 2) % tthis.states.length]; return {key: state.charCode, char: state.char, event: tthis.event, focus: tthis._focus}; } return {event: tthis.event, focus: tthis._focus}; }; this.containsCoord = function(x, y) { for (var i = 0; i < this.bounds.length; i++) { if (x >= this.bounds[i][BOUND_X] && x <= (this.bounds[i][BOUND_X] + this.bounds[i][BOUND_W]) && y >= this.bounds[i][BOUND_Y] && y <= (this.bounds[i][BOUND_Y] + this.bounds[i][BOUND_H])) { return true; } } return false; }; this.updateState = function() { tthis.setState(eval('KBD_' + (tthis.keyboard.shift ? 'UPPERCASE' : 'LOWERCASE') + '_' + (tthis._focus ? 'HOVER' : 'DEFAULT'))); }; this.updateColor = function() { var colorIntensity = this._beingPressed ? 128 : 255; for (var i = 0; i < tthis.bounds.length; i++) { Overlays.editOverlay(tthis.overlays[i], {color: {red: colorIntensity, green: colorIntensity, blue: colorIntensity}} ); } }; this.press = function() { tthis._beingPressed = true; tthis.updateColor(); }; this.release = function() { tthis._beingPressed = false; tthis.updateColor(); }; this.blur = function() { tthis._focus = false; tthis.updateState(); }; this.focus = function() { tthis._focus = true; tthis.updateState(); }; this.setState = function(state) { tthis.keyState = state; for (var i = 0; i < tthis.bounds.length; i++) { Overlays.editOverlay(tthis.overlays[i], { subImage: {width: tthis.bounds[i][BOUND_W], height: tthis.bounds[i][BOUND_H], x: tthis.bounds[i][BOUND_X], y: (KEYBOARD_HEIGHT * tthis.keyState) + tthis.bounds[i][BOUND_Y]} }); } }; this.updateVisibility = function() { for (var i = 0; i < tthis.bounds.length; i++) { Overlays.editOverlay(tthis.overlays[i], {visible: tthis.keyboard.visible}); } }; this.rescale = function() { for (var i = 0; i < tthis.bounds.length; i++) { Overlays.editOverlay(tthis.overlays[i], { x: tthis.keyboard.getX() + tthis.bounds[i][BOUND_X] * keyboard.scale, y: tthis.keyboard.getY() + tthis.bounds[i][BOUND_Y] * keyboard.scale, width: this.bounds[i][BOUND_W] * keyboard.scale, height: this.bounds[i][BOUND_H] * keyboard.scale }); } }; this.remove = function() { for (var i = 0; i < this.overlays.length; i++) { Overlays.deleteOverlay(this.overlays[i]); } }; this.isLoaded = function() { for (var i = 0; i < this.overlays.length; i++) { if (!Overlays.isLoaded(this.overlays[i])) { return false; } } return true; }; for (var i = 0; i < this.bounds.length; i++) { if (THREE_D_MODE) { this.overlays.push(Overlays.addOverlay("image3d", { scale: 1, rotation: MyAvatar.rotation, isFacingAvatar: false, url: KEYBOARD_URL, alpha: 1, position: { x: MyAvatar.position.x,// + this.bounds[i][BOUND_X] * 0.01,// /*+ this.keyboard.getX()*/ + this.bounds[i][BOUND_X] * keyboard.scale, y: MyAvatar.position.y,// - this.bounds[i][BOUND_Y] * 0.01,// /*+ this.keyboard.getY()*/ + this.bounds[i][BOUND_Y] * keyboard.scale, z: MyAvatar.position.z }, width: this.bounds[i][BOUND_W] * keyboard.scale, height: this.bounds[i][BOUND_H] * keyboard.scale, subImage: {width: this.bounds[i][BOUND_W], height: this.bounds[i][BOUND_H], x: this.bounds[i][BOUND_X], y: (KEYBOARD_HEIGHT * this.keyState) + this.bounds[i][BOUND_Y]}, alpha: 1, visible: tthis.keyboard.visible })); } else { this.overlays.push(Overlays.addOverlay("image", { imageURL: KEYBOARD_URL, x: this.keyboard.getX() + this.bounds[i][BOUND_X] * keyboard.scale, y: this.keyboard.getY() + this.bounds[i][BOUND_Y] * keyboard.scale, width: this.bounds[i][BOUND_W] * keyboard.scale, height: this.bounds[i][BOUND_H] * keyboard.scale, subImage: {width: this.bounds[i][BOUND_W], height: this.bounds[i][BOUND_H], x: this.bounds[i][BOUND_X], y: (KEYBOARD_HEIGHT * this.keyState) + this.bounds[i][BOUND_Y]}, alpha: 1, visible: tthis.keyboard.visible })); } } }); Keyboard = (function(params) { if (params === undefined) { params = {}; } var tthis = this; this.focussed_key = -1; this.scale = (windowDimensions.x / KEYBOARD_WIDTH) * KEYBOARD_SCALE_MULTIPLIER; this.shift = false; this.visible = params.visible != undefined ? params.visible : true; this.width = function() { return KEYBOARD_WIDTH * tthis.scale; }; this.height = function() { return KEYBOARD_HEIGHT * tthis.scale; }; this.getX = function() { return (windowDimensions.x / 2) - (this.width() / 2); }; this.getY = function() { return windowDimensions.y - this.height(); }; if (THREE_D_MODE) { this.background = Overlays.addOverlay("image3d", { scale: 1, position: MyAvatar.position, rotation: MyAvatar.rotation, width: this.width(), height: this.height(), subImage: {width: KEYBOARD_WIDTH, height: KEYBOARD_HEIGHT, y: KEYBOARD_HEIGHT * KBD_BACKGROUND}, isFacingAvatar: false, url: KEYBOARD_URL, alpha: 1, visible: this.visible }); } else { this.background = Overlays.addOverlay("image", { x: this.getX(), y: this.getY(), width: this.width(), height: this.height(), subImage: {width: KEYBOARD_WIDTH, height: KEYBOARD_HEIGHT, y: KEYBOARD_HEIGHT * KBD_BACKGROUND}, imageURL: KEYBOARD_URL, alpha: 1, visible: this.visible }); } this.rescale = function() { this.scale = (windowDimensions.x / KEYBOARD_WIDTH) * KEYBOARD_SCALE_MULTIPLIER; Overlays.editOverlay(tthis.background, { x: this.getX(), y: this.getY(), width: this.width(), height: this.height() }); for (var i = 0; i < tthis.keys.length; i++) { tthis.keys[i].rescale(); } }; this.setFocusPosition = function(x, y) { // set to local unscaled position var localx = (x - tthis.getX()) / tthis.scale; var localy = (y - tthis.getY()) / tthis.scale; var new_focus_key = -1; if (localx >= 0 && localy >= 0 && localx <= KEYBOARD_WIDTH && localy <= KEYBOARD_HEIGHT) { for (var i = 0; i < tthis.keys.length; i++) { if (tthis.keys[i].containsCoord(localx, localy)) { new_focus_key = i; break; } } } if (new_focus_key != tthis.focussed_key) { if (tthis.focussed_key != -1) { tthis.keys[tthis.focussed_key].blur(); } tthis.focussed_key = new_focus_key; if (tthis.focussed_key != -1) { tthis.keys[tthis.focussed_key].focus(); } } return tthis; }; this.pressFocussedKey = function() { if (!tthis.visible) { return tthis; } if (tthis.focussed_key != -1) { if (tthis.keys[tthis.focussed_key].event == 'shift') { tthis.toggleShift(); } else { tthis.keys[tthis.focussed_key].press(); } if (this.onKeyPress != null) { this.onKeyPress(tthis.keys[tthis.focussed_key].getKeyEvent()); } } return tthis; }; this.releaseKeys = function() { for (var i = 0; i < tthis.keys.length; i++) { if (tthis.keys[i]._beingPressed) { if (tthis.keys[i].event != 'shift') { tthis.keys[i].release(); } if (this.onKeyRelease != null) { this.onKeyRelease(tthis.keys[i].getKeyEvent()); } } } }; this.toggleShift = function() { tthis.shift = !tthis.shift; for (var i = 0; i < tthis.keys.length; i++) { tthis.keys[i].updateState(); if (tthis.keys[i].event == 'shift') { if (tthis.shift) { tthis.keys[i].press(); continue; } tthis.keys[i].release(); } } }; this.getFocussedKey = function() { if (tthis.focussed_key == -1) { return null; } return tthis.keys[tthis.focussed_key]; }; this.remove = function() { Overlays.deleteOverlay(this.background); for (var i = 0; i < this.keys.length; i++) { this.keys[i].remove(); } // resets the cursor and magnifier this.updateVisibility(false); }; this.show = function() { tthis.updateVisibility(true); }; this.hide = function() { tthis.updateVisibility(false); }; this.toggle = function() { tthis.updateVisibility(!tthis.visible); }; this.updateVisibility = function(visible) { tthis.visible = visible; if (HMD.magnifier == visible) { HMD.toggleMagnifier(); } Overlays.editOverlay(tthis.background, { visible: tthis.visible }); for (var i = 0; i < this.keys.length; i++) { this.keys[i].updateVisibility(); } }; this.onKeyPress = null; this.onKeyRelease = null; this.onSubmit = null; this.onFullyLoaded = null; this.keys = []; // // keyProperties contains the key data // // coords [[x,y,w,h],[x,y,w,h]] // states array of 1 or 2 objects [lowercase, uppercase] each object contains a charCode and a char var keyProperties = [ {bounds: [[12, 12, 65, 52]], states: [{charCode: 126, char: '~'}]}, {bounds: [[84, 12, 65, 52]], states: [{charCode: 33, char: '!'}]}, {bounds: [[156, 12, 65, 52]], states: [{charCode: 64, char: '@'}]}, {bounds: [[228, 12, 65, 52]], states: [{charCode: 35, char: '#'}]}, {bounds: [[300, 12, 65, 52]], states: [{charCode: 36, char: '$'}]}, {bounds: [[372, 12, 65, 52]], states: [{charCode: 37, char: '%'}]}, {bounds: [[445, 12, 65, 52]], states: [{charCode: 94, char: '^'}]}, {bounds: [[517, 12, 65, 52]], states: [{charCode: 38, char: '&'}]}, {bounds: [[589, 12, 65, 52]], states: [{charCode: 42, char: '*'}]}, {bounds: [[662, 12, 65, 52]], states: [{charCode: 40, char: '('}]}, {bounds: [[734, 12, 65, 52]], states: [{charCode: 41, char: ')'}]}, {bounds: [[806, 12, 65, 52]], states: [{charCode: 95, char: '_'}]}, {bounds: [[881, 12, 65, 52]], states: [{charCode: 123, char: '{'}]}, {bounds: [[953, 12, 65, 52]], states: [{charCode: 125, char: '}'}]}, {bounds: [[1025, 12, 65, 52]], states: [{charCode: 60, char: '<'}]}, {bounds: [[1097, 12, 65, 52]], states: [{charCode: 62, char: '>'}]}, {bounds: [[12, 71, 65, 63]], states: [{charCode: 96, char: '`'}]}, {bounds: [[84, 71, 65, 63]], states: [{charCode: 49, char: '1'}]}, {bounds: [[156, 71, 65, 63]], states: [{charCode: 50, char: '2'}]}, {bounds: [[228, 71, 65, 63]], states: [{charCode: 51, char: '3'}]}, {bounds: [[300, 71, 65, 63]], states: [{charCode: 52, char: '4'}]}, {bounds: [[372, 71, 65, 63]], states: [{charCode: 53, char: '5'}]}, {bounds: [[445, 71, 65, 63]], states: [{charCode: 54, char: '6'}]}, {bounds: [[517, 71, 65, 63]], states: [{charCode: 55, char: '7'}]}, {bounds: [[589, 71, 65, 63]], states: [{charCode: 56, char: '8'}]}, {bounds: [[661, 71, 65, 63]], states: [{charCode: 57, char: '9'}]}, {bounds: [[733, 71, 65, 63]], states: [{charCode: 48, char: '0'}]}, {bounds: [[806, 71, 65, 63]], states: [{charCode: 45, char: '-'}]}, {bounds: [[880, 71, 65, 63]], states: [{charCode: 61, char: '='}]}, {bounds: [[953, 71, 65, 63]], states: [{charCode: 43, char: '+'}]}, {bounds: [[1024, 71, 139, 63]], event: 'delete'}, // enter key has 2 bounds and one state {bounds: [[11, 143, 98, 71], [11, 213, 121, 62]], event: 'enter'}, {bounds: [[118, 142, 64, 63]], states: [{charCode: 113, char: 'q'}, {charCode: 81, char: 'Q'}]}, {bounds: [[190, 142, 64, 63]], states: [{charCode: 119, char: 'w'}, {charCode: 87, char: 'W'}]}, {bounds: [[262, 142, 64, 63]], states: [{charCode: 101, char: 'e'}, {charCode: 69, char: 'E'}]}, {bounds: [[334, 142, 64, 63]], states: [{charCode: 114, char: 'r'}, {charCode: 82, char: 'R'}]}, {bounds: [[407, 142, 64, 63]], states: [{charCode: 116, char: 't'}, {charCode: 84, char: 'T'}]}, {bounds: [[479, 142, 64, 63]], states: [{charCode: 121, char: 'y'}, {charCode: 89, char: 'Y'}]}, {bounds: [[551, 142, 65, 63]], states: [{charCode: 117, char: 'u'}, {charCode: 85, char: 'U'}]}, {bounds: [[623, 142, 65, 63]], states: [{charCode: 105, char: 'i'}, {charCode: 73, char: 'I'}]}, {bounds: [[695, 142, 65, 63]], states: [{charCode: 111, char: 'o'}, {charCode: 79, char: 'O'}]}, {bounds: [[768, 142, 64, 63]], states: [{charCode: 112, char: 'p'}, {charCode: 80, char: 'P'}]}, {bounds: [[840, 142, 64, 63]], states: [{charCode: 91, char: '['}]}, {bounds: [[912, 142, 65, 63]], states: [{charCode: 93, char: ']'}]}, {bounds: [[984, 142, 65, 63]], states: [{charCode: 92, char: '\\'}]}, {bounds: [[1055, 142, 65, 63]], states: [{charCode: 124, char: '|'}]}, {bounds: [[1126, 143, 35, 72], [1008, 214, 153, 62]], event: 'enter'}, {bounds: [[140, 213, 65, 63]], states: [{charCode: 97, char: 'a'}, {charCode: 65, char: 'A'}]}, {bounds: [[211, 213, 64, 63]], states: [{charCode: 115, char: 's'}, {charCode: 83, char: 'S'}]}, {bounds: [[283, 213, 65, 63]], states: [{charCode: 100, char: 'd'}, {charCode: 68, char: 'D'}]}, {bounds: [[355, 213, 65, 63]], states: [{charCode: 102, char: 'f'}, {charCode: 70, char: 'F'}]}, {bounds: [[428, 213, 64, 63]], states: [{charCode: 103, char: 'g'}, {charCode: 71, char: 'G'}]}, {bounds: [[500, 213, 64, 63]], states: [{charCode: 104, char: 'h'}, {charCode: 72, char: 'H'}]}, {bounds: [[572, 213, 65, 63]], states: [{charCode: 106, char: 'j'}, {charCode: 74, char: 'J'}]}, {bounds: [[644, 213, 65, 63]], states: [{charCode: 107, char: 'k'}, {charCode: 75, char: 'K'}]}, {bounds: [[716, 213, 65, 63]], states: [{charCode: 108, char: 'l'}, {charCode: 76, char: 'L'}]}, {bounds: [[789, 213, 64, 63]], states: [{charCode: 59, char: ';'}]}, {bounds: [[861, 213, 64, 63]], states: [{charCode: 39, char: '\''}]}, {bounds: [[934, 213, 65, 63]], states: [{charCode: 58, char: ':'}]}, {bounds: [[12, 283, 157, 63]], event: 'shift'}, {bounds: [[176, 283, 65, 63]], states: [{charCode: 122, char: 'z'}, {charCode: 90, char: 'Z'}]}, {bounds: [[249, 283, 64, 63]], states: [{charCode: 120, char: 'x'}, {charCode: 88, char: 'X'}]}, {bounds: [[321, 283, 64, 63]], states: [{charCode: 99, char: 'c'}, {charCode: 67, char: 'C'}]}, {bounds: [[393, 283, 64, 63]], states: [{charCode: 118, char: 'v'}, {charCode: 86, char: 'V'}]}, {bounds: [[465, 283, 65, 63]], states: [{charCode: 98, char: 'b'}, {charCode: 66, char: 'B'}]}, {bounds: [[537, 283, 65, 63]], states: [{charCode: 110, char: 'n'}, {charCode: 78, char: 'N'}]}, {bounds: [[610, 283, 64, 63]], states: [{charCode: 109, char: 'm'}, {charCode: 77, char: 'M'}]}, {bounds: [[682, 283, 64, 63]], states: [{charCode: 44, char: ','}]}, {bounds: [[754, 283, 65, 63]], states: [{charCode: 46, char: '.'}]}, {bounds: [[826, 283, 65, 63]], states: [{charCode: 47, char: '/'}]}, {bounds: [[899, 283, 64, 63]], states: [{charCode: 63, char: '?'}]}, {bounds: [[972, 283, 190, 63]], event: 'shift'}, {bounds: [[249, 355, 573, 67]], states: [{charCode: 32, char: ' '}]}, {bounds: [[899, 355, 263, 67]], event: 'submit'} ]; for (var i = 0; i < keyProperties.length; i++) { this.keys.push(new KeyboardKey(this, keyProperties[i])); } this.keyboardTextureLoaded = function() { if (Overlays.isLoaded(tthis.background)) { Script.clearInterval(tthis.keyboardTextureLoaded_timer); if (keyboard.onFullyLoaded != null) { tthis.onFullyLoaded(); } } }; this.keyboardTextureLoaded_timer = Script.setInterval(this.keyboardTextureLoaded, 250); }); Cursor = (function(params) { if (params === undefined) { params = {}; } var tthis = this; this.initialize = function() { this.visible = params.visible != undefined ? params.visible : true; this.x = windowDimensions.x / 2; this.y = windowDimensions.y / 2; this.overlay = Overlays.addOverlay("image", { x: this.x, y: this.y, width: CURSOR_WIDTH, height: CURSOR_HEIGHT, imageURL: CURSOR_URL, alpha: 1, visible: this.visible }); this.remove = function() { Overlays.deleteOverlay(this.overlay); }; this.getPosition = function() { return {x: tthis.getX(), y: tthis.getY()}; }; this.getX = function() { return tthis.x; }; this.getY = function() { return tthis.y; }; this.show = function() { tthis.updateVisibility(true); }; this.hide = function() { tthis.updateVisibility(false); }; this.toggle = function() { tthis.updateVisibility(!tthis.visible); }; this.updateVisibility = function(visible) { tthis.visible = visible; Overlays.editOverlay(this.overlay, { visible: tthis.visible }); }; this.onUpdate = null; this.update = function() { var newWindowDimensions = Controller.getViewportDimensions(); if (newWindowDimensions.x != windowDimensions.x || newWindowDimensions.y != windowDimensions.y) { windowDimensions = newWindowDimensions; keyboard.rescale(); Overlays.editOverlay(text, { y: windowDimensions.y - keyboard.height() - 260, width: windowDimensions.x }); } var editobject = {}; var hudLookatPosition = HMD.getHUDLookAtPosition2D(); if (hudLookatPosition === null) { return; } if (tthis.x !== hudLookatPosition.x) { tthis.x = hudLookatPosition.x; editobject.x = tthis.x - (CURSOR_WIDTH / 2); } if (tthis.y !== hudLookatPosition.y) { tthis.y = hudLookatPosition.y; editobject.y = tthis.y - (CURSOR_HEIGHT / 2); } if (Object.keys(editobject).length > 0) { Overlays.editOverlay(tthis.overlay, editobject); if (tthis.onUpdate != null) { tthis.onUpdate(tthis.getPosition()); } } }; Script.update.connect(this.update); }; });