├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── documents └── assets │ ├── favicon.png │ ├── github-banner.png │ ├── logo.png │ └── logo.psd ├── jsdoc.conf.json ├── mocha.spec.js ├── package.json ├── sources ├── .babelrc ├── .eslintrc.js ├── devices │ ├── audio │ │ ├── Audio.js │ │ └── NullAudio.js │ ├── inputs │ │ ├── AggregateInput.js │ │ ├── AggregateInput.test.js │ │ ├── Input.js │ │ ├── Input.test.js │ │ ├── KeyboardInput.js │ │ ├── KeyboardInput.test.js │ │ ├── ManualInput.js │ │ ├── ManualInput.test.js │ │ └── NullInput.js │ ├── screens │ │ ├── NullScreen.js │ │ ├── Screen.js │ │ ├── Screen.test.js │ │ └── WebGLScreen.js │ └── timers │ │ ├── AnimationFrameTimer.js │ │ ├── AsyncTimer.js │ │ ├── AsyncTimer.test.js │ │ ├── ImmediateTimer.js │ │ ├── NullTimer.js │ │ ├── SerialTimer.js │ │ ├── SerialTimer.test.js │ │ ├── TimeoutTimer.js │ │ ├── Timer.js │ │ ├── Timer.test.js │ │ └── utils.js ├── index.js ├── mixins │ └── EmitterMixin.js └── utils │ ├── DataUtils.js │ ├── FormatUtils.js │ ├── MemoryUtils.js │ ├── ObjectUtils.js │ └── TypeUtils.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /documentation/ 3 | 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /documents/ 2 | /sources/ 3 | /documentation/ 4 | 5 | /webpack.config.js 6 | /mocha.spec.js 7 | /jsdoc.conf.json 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright © 2014 Maël Nison** 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Virt.js](http://arcanis.github.io/virtjs/documents/assets/logo.png) 2 | 3 | ![](http://arcanis.github.io/virtjs/documents/assets/github-banner.png) 4 | 5 | > Virtjs is a free collection of useful standard devices, that can be used to power various engine that makes use of the exposed interfaces. 6 | 7 | ## Supported Devices 8 | 9 | - [Audio](https://arcanis.github.io/virtjs/documentation/Audio.html) 10 | - [`NullAudio`](https://arcanis.github.io/virtjs/documentation/NullAudio.html) 11 | - [`TaiselAudio`](https://github.com/start9/taisel) (external, via TaiselAudio) 12 | 13 | - [Input](https://arcanis.github.io/virtjs/documentation/Input.html) 14 | - [`NullInput`](https://arcanis.github.io/virtjs/documentation/NullInput.html) 15 | - [`ManualInput`](https://arcanis.github.io/virtjs/documentation/ManualInput.html) 16 | - [`KeyboardInput`](https://arcanis.github.io/virtjs/documentation/KeyboardInput.html) 17 | 18 | - [Screen](https://arcanis.github.io/virtjs/documentation/Screen.html) 19 | - [`NullScreen`](https://arcanis.github.io/virtjs/documentation/NullScreen.html) 20 | - [`WebGLScreen`](https://arcanis.github.io/virtjs/documentation/WebGLScreen.html) 21 | - [`PProcScreen`](https://github.com/start9/pprocjs) (external, via PProcJS) 22 | 23 | - [Timer](https://arcanis.github.io/virtjs/documentation/Timer.html) 24 | - [`NullTimer`](https://arcanis.github.io/virtjs/documentation/NullTimer.html) 25 | - [`SerialTimer`](https://arcanis.github.io/virtjs/documentation/SerialTimer.html) 26 | - [`AsyncTimer`](https://arcanis.github.io/virtjs/documentation/AsyncTimer.html) 27 | - [`AnimationFrameTimer`](https://arcanis.github.io/virtjs/documentation/AnimationFrameTimer.html) 28 | - [`ImmediateTimer`](https://arcanis.github.io/virtjs/documentation/ImmediateTimer.html) 29 | 30 | ## Compatible With 31 | 32 | - [Archjs](https://github.com/start9/archjs) engines (Gambatte, VBA-Next, ...) 33 | 34 | ## Maintainer 35 | 36 | Virtjs is maintained by Maël Nison ([@arcanis](https://twitter.com/arcanis) on Twitter). 37 | 38 | ## License (MIT) 39 | 40 | > **Copyright © 2016 Maël Nison** 41 | > 42 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 43 | > 44 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 45 | > 46 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 47 | -------------------------------------------------------------------------------- /documents/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/virtjs/884be4a0d81cccb1f6307f1c536ebcd772d8be98/documents/assets/favicon.png -------------------------------------------------------------------------------- /documents/assets/github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/virtjs/884be4a0d81cccb1f6307f1c536ebcd772d8be98/documents/assets/github-banner.png -------------------------------------------------------------------------------- /documents/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/virtjs/884be4a0d81cccb1f6307f1c536ebcd772d8be98/documents/assets/logo.png -------------------------------------------------------------------------------- /documents/assets/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/virtjs/884be4a0d81cccb1f6307f1c536ebcd772d8be98/documents/assets/logo.psd -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "plugins": ["plugins/markdown"], 6 | "templates": { 7 | "logoFile": "", 8 | "cleverLinks": false, 9 | "monospaceLinks": false, 10 | "dateFormat": "ddd MMM Do YYYY", 11 | "outputSourceFiles": true, 12 | "outputSourcePath": true, 13 | "systemName": "Virtjs", 14 | "footer": "", 15 | "copyright": "Virtjs Copyright © 2012-2016 The contributors to the Virtjs project.", 16 | "navType": "vertical", 17 | "theme": "simplex", 18 | "linenums": true, 19 | "collapseSymbols": false, 20 | "inverseNav": true, 21 | "protocol": "html://", 22 | "methodHeadingReturns": false 23 | }, 24 | "markdown": { 25 | "parser": "gfm", 26 | "hardwrap": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mocha.spec.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom'); 2 | 3 | var chai = require('chai'); 4 | var spies = require('chai-spies'); 5 | 6 | chai.use(spies); 7 | 8 | global.chai = chai; 9 | global.expect = chai.expect; 10 | 11 | beforeEach(function () { 12 | global.document = jsdom.jsdom(); 13 | }); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtjs", 3 | "description": "Javascript emulation library", 4 | "version": "0.5.0", 5 | "main": "./build/main.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "prepublish": "npm run build", 9 | "doc": "jsdoc --destination documentation -c jsdoc.conf.json -t ./node_modules/ink-docstrap/template -r sources", 10 | "lint": "find sources \\( -name '*.js' -a -not -name '*.test.js' -a -not -name '.eslintrc.js' \\) -exec eslint {} +", 11 | "test": "NODE_ENV=test xvfb-run -s '-ac -screen 0 1280x1024x24' mocha mocha.spec.js sources/**/*.test.js --compilers js:babel-register --require babel-polyfill", 12 | "publish-doc": "(git checkout gh-pages && git reset --hard master && npm run doc && git add -f documentation && git commit -m 'Generates documentation' && git push --set-upstream origin gh-pages && git checkout -)" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^6.8.0", 16 | "babel-eslint": "^6.0.4", 17 | "babel-loader": "^6.2.4", 18 | "babel-plugin-webpack-alias": "^1.3.0", 19 | "babel-polyfill": "^6.8.0", 20 | "babel-preset-es2015": "^6.6.0", 21 | "babel-preset-stage-0": "^6.5.0", 22 | "babel-register": "^6.8.0", 23 | "canvas": "^1.3.15", 24 | "chai": "^3.5.0", 25 | "chai-spies": "^0.7.1", 26 | "eslint": "^2.10.0", 27 | "eslint-import-resolver-webpack": "^0.2.4", 28 | "eslint-plugin-arca": "^0.5.1", 29 | "eslint-plugin-import": "^1.8.0", 30 | "gl": "^3.0.5", 31 | "ink-docstrap": "^1.1.4", 32 | "jsdoc": "^3.4.0", 33 | "jsdom": "^9.0.0", 34 | "keysim": "^1.3.0", 35 | "mocha": "^2.4.5", 36 | "webpack": "^1.13.0" 37 | }, 38 | "peerDependencies": { 39 | "babel-polyfill": "^6.8.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sources/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 'es2015', 'stage-0' ], 3 | 4 | env: { 5 | test: { 6 | plugins: [ 7 | [ 'babel-plugin-webpack-alias', { 8 | config: 'webpack.config.js', 9 | findConfig: true 10 | } ] 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sources/.eslintrc.js: -------------------------------------------------------------------------------- 1 | var WARNING = 1; 2 | var ERROR = 2; 3 | 4 | module.exports = { 5 | 6 | parser: "babel-eslint", 7 | 8 | env: { 9 | "browser": true, 10 | "es6": true, 11 | "node": true 12 | }, 13 | 14 | globals: { 15 | "module": true, 16 | "require": true 17 | }, 18 | 19 | parserOptions: { 20 | sourceType: "module", 21 | ecmaFeatures: { 22 | blockBindings: true, 23 | experimentalObjectRestSpread: true, 24 | jsx: true 25 | } 26 | }, 27 | 28 | plugins: [ 29 | "arca", 30 | "import" 31 | ], 32 | 33 | settings: { 34 | "import/resolver": "webpack", 35 | "import/ignore": [ "node_modules", "\.(scss|jpg|gif|json)$" ] 36 | }, 37 | 38 | rules: { 39 | 40 | //////////////////////// 41 | // POSSIBLE ERRORS 42 | 43 | "no-cond-assign": [ 44 | ERROR 45 | ], 46 | "no-console": [ 47 | ERROR 48 | ], 49 | "no-debugger": [ 50 | ERROR 51 | ], 52 | "no-dupe-args": [ 53 | ERROR 54 | ], 55 | "no-dupe-keys": [ 56 | ERROR 57 | ], 58 | "no-duplicate-case": [ 59 | ERROR 60 | ], 61 | "no-empty": [ 62 | ERROR 63 | ], 64 | "no-extra-boolean-cast": [ 65 | ERROR 66 | ], 67 | "no-extra-parens": [ 68 | ERROR 69 | ], 70 | "no-extra-semi": [ 71 | ERROR 72 | ], 73 | "no-func-assign": [ 74 | ERROR 75 | ], 76 | "no-invalid-regexp": [ 77 | ERROR 78 | ], 79 | "no-irregular-whitespace": [ 80 | ERROR 81 | ], 82 | "no-negated-in-lhs": [ 83 | ERROR 84 | ], 85 | "no-sparse-arrays": [ 86 | ERROR 87 | ], 88 | "no-unexpected-multiline": [ 89 | ERROR 90 | ], 91 | "no-unreachable": [ 92 | ERROR 93 | ], 94 | "use-isnan": [ 95 | ERROR 96 | ], 97 | "valid-typeof": [ 98 | ERROR 99 | ], 100 | 101 | "array-callback-return": [ 102 | ERROR 103 | ], 104 | "consistent-return": [ 105 | ERROR 106 | ], 107 | "dot-location": [ 108 | ERROR, 109 | "property" 110 | ], 111 | "eqeqeq": [ 112 | ERROR, 113 | "allow-null" 114 | ], 115 | "no-caller": [ 116 | ERROR 117 | ], 118 | "no-case-declarations": [ 119 | ERROR 120 | ], 121 | "no-empty-function": [ 122 | ERROR, 123 | { allow: [ "arrowFunctions" ] } 124 | ], 125 | "no-eq-null": [ 126 | ERROR 127 | ], 128 | "no-eval": [ 129 | ERROR 130 | ], 131 | "no-extend-native": [ 132 | ERROR 133 | ], 134 | "no-extra-bind": [ 135 | ERROR 136 | ], 137 | "no-extra-label": [ 138 | ERROR 139 | ], 140 | "no-fallthrough": [ 141 | ERROR 142 | ], 143 | "no-floating-decimal": [ 144 | ERROR 145 | ], 146 | "no-implicit-coercion": [ 147 | ERROR 148 | ], 149 | "no-implied-eval": [ 150 | ERROR 151 | ], 152 | "no-labels": [ 153 | ERROR 154 | ], 155 | "no-lone-blocks": [ 156 | ERROR 157 | ], 158 | "no-magic-numbers": [ 159 | ERROR, 160 | { ignore: [ -1, 0, 1, 2, 100 ] } 161 | ], 162 | "no-multi-spaces": [ 163 | ERROR, 164 | { exceptions: { VariableDeclarator: true, ImportDeclaration: true } } 165 | ], 166 | "no-multi-str": [ 167 | ERROR 168 | ], 169 | "no-native-reassign": [ 170 | ERROR 171 | ], 172 | "no-new": [ 173 | ERROR 174 | ], 175 | "no-new-func": [ 176 | ERROR 177 | ], 178 | "no-new-wrappers": [ 179 | ERROR 180 | ], 181 | "no-octal": [ 182 | ERROR 183 | ], 184 | "no-proto": [ 185 | ERROR 186 | ], 187 | "no-return-assign": [ 188 | ERROR 189 | ], 190 | "no-script-url": [ 191 | ERROR 192 | ], 193 | "no-self-assign": [ 194 | ERROR 195 | ], 196 | "no-self-compare": [ 197 | ERROR 198 | ], 199 | "no-sequences": [ 200 | ERROR 201 | ], 202 | "no-throw-literal": [ 203 | ERROR 204 | ], 205 | "no-unmodified-loop-condition": [ 206 | ERROR 207 | ], 208 | "no-unused-expressions": [ 209 | ERROR, 210 | { allowShortCircuit: true } 211 | ], 212 | "no-useless-call": [ 213 | ERROR 214 | ], 215 | "no-useless-concat": [ 216 | ERROR 217 | ], 218 | "no-void": [ 219 | ERROR 220 | ], 221 | "no-warning-comments": [ 222 | WARNING 223 | ], 224 | "no-with": [ 225 | ERROR 226 | ], 227 | "radix": [ 228 | ERROR 229 | ], 230 | "wrap-iife": [ 231 | ERROR 232 | ], 233 | "yoda": [ 234 | ERROR, 235 | "never" 236 | ], 237 | 238 | //////////////////////// 239 | // VARIABLES 240 | 241 | "no-catch-shadow": [ 242 | ERROR 243 | ], 244 | "no-delete-var": [ 245 | ERROR 246 | ], 247 | "no-restricted-globals": [ 248 | ERROR, 249 | "event" 250 | ], 251 | "no-shadow": [ 252 | ERROR 253 | ], 254 | "no-shadow-restricted-names": [ 255 | ERROR 256 | ], 257 | "no-undef": [ 258 | ERROR 259 | ], 260 | "no-undefined": [ 261 | ERROR 262 | ], 263 | "no-unused-vars": [ 264 | ERROR, 265 | { args: "none" } 266 | ], 267 | "no-use-before-define": [ 268 | ERROR 269 | ], 270 | 271 | //////////////////////// 272 | // STYLISTIC ISSUES 273 | 274 | "array-bracket-spacing": [ 275 | ERROR, 276 | "always" 277 | ], 278 | "block-spacing": [ 279 | ERROR, 280 | "always" 281 | ], 282 | "brace-style": [ 283 | ERROR 284 | ], 285 | "camelcase": [ 286 | ERROR, 287 | { properties: "never" } 288 | ], 289 | "comma-spacing": [ 290 | ERROR 291 | ], 292 | "comma-style": [ 293 | ERROR 294 | ], 295 | "computed-property-spacing": [ 296 | ERROR 297 | ], 298 | "eol-last": [ 299 | ERROR 300 | ], 301 | "func-names": [ 302 | ERROR 303 | ], 304 | "func-style": [ 305 | ERROR, 306 | "declaration", 307 | { allowArrowFunctions: true } 308 | ], 309 | "indent": [ 310 | ERROR, 311 | 4, 312 | { SwitchCase: 1 } 313 | ], 314 | "key-spacing": [ 315 | ERROR 316 | ], 317 | "linebreak-style": [ 318 | ERROR, 319 | "unix" 320 | ], 321 | "lines-around-comment": [ 322 | ERROR 323 | ], 324 | "new-cap": [ 325 | ERROR 326 | ], 327 | "new-parens": [ 328 | ERROR 329 | ], 330 | "no-array-constructor": [ 331 | ERROR 332 | ], 333 | "no-lonely-if": [ 334 | ERROR 335 | ], 336 | "no-mixed-spaces-and-tabs": [ 337 | ERROR 338 | ], 339 | "no-multiple-empty-lines": [ 340 | ERROR 341 | ], 342 | "no-nested-ternary": [ 343 | ERROR 344 | ], 345 | "no-new-object": [ 346 | ERROR 347 | ], 348 | "no-spaced-func": [ 349 | ERROR 350 | ], 351 | "no-trailing-spaces": [ 352 | ERROR 353 | ], 354 | "no-underscore-dangle": [ 355 | ERROR 356 | ], 357 | "no-whitespace-before-property": [ 358 | ERROR 359 | ], 360 | "object-curly-spacing": [ 361 | ERROR, 362 | "always" 363 | ], 364 | "one-var": [ 365 | ERROR, 366 | "never" 367 | ], 368 | "padded-blocks": [ 369 | ERROR, 370 | { switches: "always", classes: "always" } 371 | ], 372 | "quote-props": [ 373 | ERROR, 374 | "consistent-as-needed" 375 | ], 376 | "quotes": [ 377 | ERROR, 378 | "backtick" 379 | ], 380 | "semi": [ 381 | ERROR, 382 | "always" 383 | ], 384 | "semi-spacing": [ 385 | ERROR 386 | ], 387 | "space-before-blocks": [ 388 | ERROR 389 | ], 390 | "space-before-function-paren": [ 391 | ERROR, 392 | { anonymous: "always", named: "never" } 393 | ], 394 | "space-in-parens": [ 395 | ERROR 396 | ], 397 | "space-infix-ops": [ 398 | ERROR 399 | ], 400 | "space-unary-ops": [ 401 | ERROR 402 | ], 403 | "spaced-comment": [ 404 | ERROR 405 | ], 406 | 407 | //////////////////////// 408 | // ECMASCRIPT 6 409 | 410 | "arrow-spacing": [ 411 | ERROR 412 | ], 413 | "constructor-super": [ 414 | ERROR 415 | ], 416 | "generator-star-spacing": [ 417 | ERROR, 418 | { before: false, after: true } 419 | ], 420 | "no-class-assign": [ 421 | ERROR 422 | ], 423 | "no-const-assign": [ 424 | ERROR 425 | ], 426 | "no-dupe-class-members": [ 427 | ERROR 428 | ], 429 | "no-new-symbol": [ 430 | ERROR 431 | ], 432 | "no-this-before-super": [ 433 | ERROR 434 | ], 435 | "no-useless-constructor": [ 436 | ERROR 437 | ], 438 | "no-var": [ 439 | ERROR 440 | ], 441 | "prefer-arrow-callback": [ 442 | ERROR 443 | ], 444 | "prefer-reflect": [ 445 | ERROR 446 | ], 447 | "prefer-rest-params": [ 448 | ERROR 449 | ], 450 | "prefer-spread": [ 451 | ERROR 452 | ], 453 | "prefer-template": [ 454 | ERROR 455 | ], 456 | "template-curly-spacing": [ 457 | ERROR 458 | ], 459 | "yield-star-spacing": [ 460 | ERROR 461 | ], 462 | 463 | //////////////////////// 464 | // http://github.com/arcanis/eslint-plugin-arca 465 | 466 | "arca/curly": [ 467 | ERROR 468 | ], 469 | "arca/melted-constructs": [ 470 | ERROR 471 | ], 472 | "arca/import-align": [ 473 | ERROR 474 | ], 475 | "arca/import-ordering": [ 476 | ERROR, 477 | [ "^styles/", "^common/", "^(fullscreen|devtool)/", "^sources/", "^settings/" ] 478 | ], 479 | "arca/newline-after-import-section": [ 480 | ERROR, 481 | [ "^styles/", "^common/", "^(fullscreen|devtool)/", "^sources/", "^settings/" ] 482 | ], 483 | "arca/no-default-export": [ 484 | ERROR 485 | ], 486 | 487 | //////////////////////// 488 | // IMPORT STATEMENTS 489 | 490 | "import/default": [ 491 | ERROR 492 | ], 493 | "import/export": [ 494 | ERROR 495 | ], 496 | "import/imports-first": [ 497 | ERROR 498 | ], 499 | "import/named": [ 500 | ERROR 501 | ], 502 | "import/no-amd": [ 503 | ERROR 504 | ], 505 | "import/no-commonjs": [ 506 | ERROR 507 | ], 508 | "import/no-unresolved": [ 509 | ERROR 510 | ] 511 | 512 | } 513 | }; 514 | -------------------------------------------------------------------------------- /sources/devices/audio/Audio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Audio 3 | * @interface 4 | */ 5 | 6 | /** 7 | * An engine will call this function to check if the device supports the specified input format. 8 | * 9 | * Return true if the audio device supports the specified input format. 10 | * 11 | * @method 12 | * @name Audio#validateInputFormat 13 | * 14 | * @param {AudioInputFormat} format - The input format to validate. 15 | */ 16 | 17 | /** 18 | * An engine will call this function to inform the device of the new input format. 19 | * 20 | * Throw an exception if the audio device doesn't support the new input format. 21 | * 22 | * @method 23 | * @name Audio#setInputFormat 24 | * 25 | * @param {AudioInputFormat} format - The new input format. 26 | */ 27 | 28 | /** 29 | * An engine will call this function to send samples that the audio device will queue to be played. 30 | * 31 | * Regardless of the input format, the samples are expected to be interlaced (for example, a mono stream will have [left, left, left], whereas a stereo stream will have [left, right, left, right, left, right]). 32 | * 33 | * @param {number[]} samples - The samples to queue. 34 | */ 35 | -------------------------------------------------------------------------------- /sources/devices/audio/NullAudio.js: -------------------------------------------------------------------------------- 1 | export class NullAudio { 2 | 3 | /** 4 | * A NullAudio is an audio device that won't play anything. 5 | * 6 | * @constructor 7 | * @implements {Audio} 8 | */ 9 | 10 | constructor() { // eslint-disable-line no-useless-constructor 11 | 12 | // nothing 13 | 14 | } 15 | 16 | validateInputFormat(format) { 17 | 18 | return true; 19 | 20 | } 21 | 22 | setInputFormat(format) { 23 | 24 | // nothing 25 | 26 | } 27 | 28 | pushSampleBatch(samples) { 29 | 30 | // nothing 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sources/devices/inputs/AggregateInput.js: -------------------------------------------------------------------------------- 1 | export class AggregateInput { 2 | 3 | /** 4 | * An AggregateInput is an input device that combines multiple other inputs together. 5 | * 6 | * @constructor 7 | * @implements {Input} 8 | * 9 | * @param {Input[]} [sources] - The initial sources used for the aggregate. 10 | */ 11 | 12 | constructor(sources = []) { 13 | 14 | this.inputs = []; 15 | 16 | for (let source of sources) { 17 | this.addSource(source); 18 | } 19 | 20 | } 21 | 22 | /** 23 | * Add a new input source inside the aggregate. 24 | * 25 | * @param {Input} input - The input to aggregate. 26 | */ 27 | 28 | addSource(input) { 29 | 30 | if (this.inputs.includes(input)) 31 | return; 32 | 33 | this.inputs.push(input); 34 | 35 | } 36 | 37 | /** 38 | * Remove an input source from the aggregate. 39 | * 40 | * @param {Input} input - The input to remove. 41 | */ 42 | 43 | removeSource(input) { 44 | 45 | let index = this.inputs.indexOf(input); 46 | this.inputs.splice(index, 1); 47 | 48 | } 49 | 50 | /** 51 | * Simultaneously poll each aggregated input. 52 | */ 53 | 54 | pollInputs() { 55 | 56 | for (let input of this.inputs) { 57 | input.pollInputs(); 58 | } 59 | 60 | } 61 | 62 | /** 63 | * Returns true if any of the aggregated input should return true. 64 | * 65 | * @param {number} port - The input slot controller. 66 | * @param {number} code - The input slot code. 67 | */ 68 | 69 | getState(port, code) { 70 | 71 | for (let input of this.inputs) 72 | if (input.getState(port, code)) 73 | return true; 74 | 75 | return false; 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /sources/devices/inputs/AggregateInput.test.js: -------------------------------------------------------------------------------- 1 | import { AggregateInput } from 'virtjs/devices/inputs/AggregateInput'; 2 | import { ManualInput } from 'virtjs/devices/inputs/ManualInput'; 3 | 4 | describe(`AggregateInput`, () => { 5 | 6 | describe(`#pollInputs()`, () => { 7 | 8 | it(`should call pollInputs() on each aggregated input`, () => { 9 | 10 | let aggregatedInputA = new ManualInput(); 11 | let aggregatedInputB = new ManualInput(); 12 | 13 | let input = new AggregateInput([ aggregatedInputA, aggregatedInputB ]); 14 | 15 | aggregatedInputA.down(0, 0); 16 | aggregatedInputB.down(1, 1); 17 | 18 | expect(aggregatedInputA.getState(0, 0)).to.be.false; 19 | expect(aggregatedInputB.getState(1, 1)).to.be.false; 20 | 21 | input.pollInputs(); 22 | 23 | expect(aggregatedInputA.getState(0, 0)).to.be.true; 24 | expect(aggregatedInputB.getState(1, 1)).to.be.true; 25 | 26 | }); 27 | 28 | }); 29 | 30 | describe(`#getState()`, () => { 31 | 32 | it(`should return true if any of the aggregated input would return true`, () => { 33 | 34 | let aggregatedInputA = new ManualInput(); 35 | let aggregatedInputB = new ManualInput(); 36 | 37 | let input = new AggregateInput([ aggregatedInputA, aggregatedInputB ]); 38 | 39 | aggregatedInputA.down(0, 0); 40 | aggregatedInputB.down(1, 1); 41 | 42 | input.pollInputs(); 43 | 44 | expect(input.getState(0, 0)).to.be.true; 45 | expect(input.getState(1, 1)).to.be.true; 46 | expect(input.getState(2, 2)).to.be.false; 47 | 48 | }); 49 | 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /sources/devices/inputs/Input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Input 3 | * @interface 4 | */ 5 | 6 | /** 7 | * An engine will call this function to inform the device that it should update the input state. 8 | * 9 | * It means that the devices should never update the input state by themselves, but rather wait for the engine order. It is also important that the update is done synchronously, so that right after returning, the engines are able to call {@link Input#getState}. 10 | * 11 | * @method 12 | * @name Input#pollInputs 13 | */ 14 | 15 | /** 16 | * An engine will call this function to check the current state of a specified input. The function will return true if the input is currently active (pressed), and false otherwise. 17 | * 18 | * @method 19 | * @name Input#getState 20 | * 21 | * @param {number} port - The input controller port. 22 | * @param {number} code - The input code. 23 | */ 24 | -------------------------------------------------------------------------------- /sources/devices/inputs/Input.test.js: -------------------------------------------------------------------------------- 1 | import { AggregateInput } from 'virtjs/devices/inputs/AggregateInput'; 2 | import { KeyboardInput } from 'virtjs/devices/inputs/KeyboardInput'; 3 | import { ManualInput } from 'virtjs/devices/inputs/ManualInput'; 4 | import { NullInput } from 'virtjs/devices/inputs/NullInput'; 5 | 6 | describe(`Input`, () => { 7 | 8 | let inputs = { AggregateInput, ManualInput, NullInput }; 9 | 10 | for (let [ name, Input ] of Object.entries(inputs)) { 11 | 12 | describe(name, () => { 13 | 14 | describe(`#pollInputs()`, () => { 15 | 16 | it(`should exists`, () => { 17 | 18 | let input = new Input(); 19 | 20 | expect(input).to.have.property(`pollInputs`); 21 | expect(input.pollInputs).to.be.a(`function`); 22 | 23 | }); 24 | 25 | }); 26 | 27 | describe(`#getState()`, () => { 28 | 29 | it(`should exists`, () => { 30 | 31 | let input = new Input(); 32 | 33 | expect(input).to.have.property(`getState`); 34 | expect(input.getState).to.be.a(`function`); 35 | 36 | }); 37 | 38 | it(`should return a boolean`, () => { 39 | 40 | let input = new Input(); 41 | 42 | expect(input.getState(0, 0)).to.be.a(`boolean`); 43 | 44 | }); 45 | 46 | }); 47 | 48 | }); 49 | 50 | } 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /sources/devices/inputs/KeyboardInput.js: -------------------------------------------------------------------------------- 1 | import { ManualInput } from 'virtjs/devices/inputs/ManualInput'; 2 | 3 | let DEFAULT_KEY_MAP = { 4 | 5 | /* eslint-disable no-magic-numbers */ 6 | 7 | 37: [ 0, `LEFT` ], // arrow left 8 | 39: [ 0, `RIGHT` ], // arrow right 9 | 38: [ 0, `UP` ], // arrow up 10 | 40: [ 0, `DOWN` ], // arrow down 11 | 12 | 65: [ 0, `A` ], // 'A' 13 | 81: [ 0, `A` ], // 'Q' 14 | 15 | 90: [ 0, `B` ], // 'Z' 16 | 87: [ 0, `B` ], // 'W' 17 | 66: [ 0, `B` ], // 'B' 18 | 19 | 76: [ 0, `L` ], // 'L' 20 | 82: [ 0, `R` ], // 'R' 21 | 22 | 13: [ 0, `START` ], // enter 23 | 24 | 8: [ 0, `SELECT` ], // backspace 25 | 32: [ 0, `SELECT` ] // space 26 | 27 | /* eslint-enable no-magic-numbers */ 28 | 29 | }; 30 | 31 | export class KeyboardInput { 32 | 33 | /** 34 | * A KeyboardInput is an input device that will monitor the keystrokes on a specified DOM element and transmit those actions to the engines. 35 | * 36 | * @constructor 37 | * @implements {Input} 38 | * 39 | * @param {object} [options] - The device options. 40 | * @param {Element} [options.element] - The element on which will be bound the DOM listeners. 41 | * @param {KeyMap} [options.keyMap] - The initial key map. 42 | */ 43 | 44 | constructor({ element = document.body, keyMap = DEFAULT_KEY_MAP, codeMap = null } = {}) { 45 | 46 | /** 47 | * This value contains the element on which the DOM listeners have been bound. 48 | * 49 | * @member 50 | * @readonly 51 | * @type {Element} 52 | */ 53 | 54 | this.element = null; 55 | 56 | /** 57 | * This value contains the current key map used to filter keys. 58 | * 59 | * @member 60 | * @readonly 61 | * @type {KeyMap} 62 | */ 63 | 64 | this.keyMap = null; 65 | 66 | /** 67 | * @borrows ManualInput#codeMap as KeyboardInput#codeMap 68 | */ 69 | 70 | Reflect.defineProperty(this, `codeMap`, { get() { 71 | return this.input.codeMap; 72 | } }); 73 | 74 | this.input = new ManualInput({ codeMap }); 75 | 76 | this.onKeyDown = this.onKeyDown.bind(this); 77 | this.onKeyUp = this.onKeyUp.bind(this); 78 | 79 | this.setKeyMap(keyMap); 80 | this.setElement(element); 81 | 82 | } 83 | 84 | /** 85 | * Change the element on which are bound the DOM listeners. 86 | */ 87 | 88 | setElement(element) { 89 | 90 | if (element === this.element) 91 | return; 92 | 93 | if (this.element !== null) 94 | this.detachEvents(); 95 | 96 | this.element = element; 97 | 98 | if (this.element !== null) { 99 | this.attachEvents(); 100 | } 101 | 102 | } 103 | 104 | /** 105 | * Set the key map that will be used to translate key codes into inputs. 106 | * 107 | * Any old key that doesn't map to the same input anymore will be automatically released. 108 | * 109 | * @param {KeyMap} keyMap - The new key map. 110 | */ 111 | 112 | setKeyMap(keyMap) { 113 | 114 | if (keyMap === this.keyMap) 115 | return; 116 | 117 | if (this.keyMap) { 118 | for (let key of Reflect.ownKeys(this.keyMap)) { 119 | 120 | let [ port, code ] = this.keyMap[key]; 121 | 122 | if (keyMap && Reflect.has(keyMap, key) && keyMap[key][0] === port && keyMap[key][1] === code) 123 | continue; 124 | 125 | this.input.up(port, code); 126 | 127 | } 128 | } 129 | 130 | this.keyMap = keyMap; 131 | 132 | } 133 | 134 | /** 135 | * @borrows ManualInput#setCodeMap as KeyboardInput#setCodeMap 136 | */ 137 | 138 | setCodeMap(codeMap) { 139 | 140 | this.input.setCodeMap(codeMap); 141 | 142 | } 143 | 144 | pollInputs() { 145 | 146 | this.input.pollInputs(); 147 | 148 | } 149 | 150 | getState(port, code) { 151 | 152 | return this.input.getState(port, code); 153 | 154 | } 155 | 156 | attachEvents() { 157 | 158 | this.element.addEventListener(`keydown`, this.onKeyDown); 159 | this.element.addEventListener(`keyup`, this.onKeyUp); 160 | 161 | } 162 | 163 | detachEvents() { 164 | 165 | this.element.removeEventListener(`keydown`, this.onKeyDown); 166 | this.element.removeEventListener(`keyup`, this.onKeyUp); 167 | 168 | } 169 | 170 | onKeyDown(e) { 171 | 172 | if ([ `select`, `input`, `textarea` ].includes(e.target.tagName.toLowerCase())) 173 | return; 174 | 175 | if (e.keyCode === 8 /* backspace */) // eslint-disable-line no-magic-numbers 176 | e.preventDefault(); 177 | 178 | if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) 179 | return; 180 | 181 | if (!Reflect.has(this.keyMap, e.keyCode)) 182 | return; 183 | 184 | e.preventDefault(); 185 | 186 | let [ port, code ] = this.keyMap[e.keyCode]; 187 | this.input.down(port, code); 188 | 189 | } 190 | 191 | onKeyUp(e) { 192 | 193 | if (!Reflect.has(this.keyMap, e.keyCode)) 194 | return; 195 | 196 | let [ port, code ] = this.keyMap[e.keyCode]; 197 | this.input.up(port, code); 198 | 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /sources/devices/inputs/KeyboardInput.test.js: -------------------------------------------------------------------------------- 1 | import keysim from 'keysim'; 2 | 3 | import { KeyboardInput } from 'virtjs/devices/inputs/KeyboardInput'; 4 | 5 | describe(`KeyboardInput`, () => { 6 | 7 | describe(`input fetching`, () => { 8 | 9 | it(`should catch every related keydown input on its element`, () => { 10 | 11 | let input = new KeyboardInput(); 12 | let keyboard = keysim.Keyboard.US_ENGLISH; 13 | 14 | for (let action of [ `left`, `right`, `up`, `down` ]) 15 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), document.body, true, keysim.KeyEvents.DOWN); 16 | 17 | input.pollInputs(); 18 | 19 | expect(input.getState(0, `LEFT`)).to.be.true; 20 | expect(input.getState(0, `RIGHT`)).to.be.true; 21 | expect(input.getState(0, `UP`)).to.be.true; 22 | expect(input.getState(0, `DOWN`)).to.be.true; 23 | 24 | }); 25 | 26 | it(`should catch every related keyup input on its element`, () => { 27 | 28 | let input = new KeyboardInput(); 29 | let keyboard = keysim.Keyboard.US_ENGLISH; 30 | 31 | for (let action of [ `left`, `right`, `up`, `down` ]) 32 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), document.body, true, keysim.KeyEvents.DOWN); 33 | 34 | input.pollInputs(); 35 | 36 | expect(input.getState(0, `LEFT`)).to.be.true; 37 | expect(input.getState(0, `RIGHT`)).to.be.true; 38 | expect(input.getState(0, `UP`)).to.be.true; 39 | expect(input.getState(0, `DOWN`)).to.be.true; 40 | 41 | for (let action of [ `left`, `right`, `up`, `down` ]) 42 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), document.body, true, keysim.KeyEvents.UP); 43 | 44 | input.pollInputs(); 45 | 46 | expect(input.getState(0, `LEFT`)).to.be.false; 47 | expect(input.getState(0, `RIGHT`)).to.be.false; 48 | expect(input.getState(0, `UP`)).to.be.false; 49 | expect(input.getState(0, `DOWN`)).to.be.false; 50 | 51 | }); 52 | 53 | }); 54 | 55 | describe(`#setElement()`, () => { 56 | 57 | it(`should set the bound element`, () => { 58 | 59 | let sub1 = document.createElement('div'); 60 | document.body.appendChild(sub1); 61 | 62 | let sub2 = document.createElement('div'); 63 | document.body.appendChild(sub2); 64 | 65 | let input = new KeyboardInput(); 66 | let keyboard = keysim.Keyboard.US_ENGLISH; 67 | 68 | input.setElement(sub1); 69 | 70 | for (let action of [ `left`, `right`, `up`, `down` ]) 71 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), sub1, true, keysim.KeyEvents.DOWN); 72 | 73 | input.pollInputs(); 74 | 75 | expect(input.getState(0, `LEFT`)).to.be.true; 76 | expect(input.getState(0, `RIGHT`)).to.be.true; 77 | expect(input.getState(0, `UP`)).to.be.true; 78 | expect(input.getState(0, `DOWN`)).to.be.true; 79 | 80 | for (let action of [ `left`, `right`, `up`, `down` ]) 81 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), sub2, true, keysim.KeyEvents.DOWN); 82 | 83 | input.pollInputs(); 84 | 85 | expect(input.getState(0, `LEFT`)).to.be.true; 86 | expect(input.getState(0, `RIGHT`)).to.be.true; 87 | expect(input.getState(0, `UP`)).to.be.true; 88 | expect(input.getState(0, `DOWN`)).to.be.true; 89 | 90 | }); 91 | 92 | it(`should be available right from the constructor`, () => { 93 | 94 | let sub1 = document.createElement('div'); 95 | document.body.appendChild(sub1); 96 | 97 | let sub2 = document.createElement('div'); 98 | document.body.appendChild(sub2); 99 | 100 | let input = new KeyboardInput({ element: sub1 }); 101 | let keyboard = keysim.Keyboard.US_ENGLISH; 102 | 103 | for (let action of [ `left`, `right`, `up`, `down` ]) 104 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), sub1, true, keysim.KeyEvents.DOWN); 105 | 106 | input.pollInputs(); 107 | 108 | expect(input.getState(0, `LEFT`)).to.be.true; 109 | expect(input.getState(0, `RIGHT`)).to.be.true; 110 | expect(input.getState(0, `UP`)).to.be.true; 111 | expect(input.getState(0, `DOWN`)).to.be.true; 112 | 113 | for (let action of [ `left`, `right`, `up`, `down` ]) 114 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForAction(action), sub2, true, keysim.KeyEvents.DOWN); 115 | 116 | input.pollInputs(); 117 | 118 | expect(input.getState(0, `LEFT`)).to.be.true; 119 | expect(input.getState(0, `RIGHT`)).to.be.true; 120 | expect(input.getState(0, `UP`)).to.be.true; 121 | expect(input.getState(0, `DOWN`)).to.be.true; 122 | 123 | }); 124 | 125 | }); 126 | 127 | describe(`#setKeyMap()`, () => { 128 | 129 | it(`should set the key map`, () => { 130 | 131 | let keyMap = { [48]: [ 0, `HELLO` ] }; 132 | 133 | let input = new KeyboardInput(); 134 | let keyboard = keysim.Keyboard.US_ENGLISH; 135 | 136 | input.setKeyMap(keyMap); 137 | 138 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForCharCode(48), document.body, true, keysim.KeyEvents.DOWN); 139 | 140 | input.pollInputs(); 141 | 142 | expect(input.getState(0, `HELLO`)).to.be.true; 143 | 144 | }); 145 | 146 | it(`should be available right from the constructor`, () => { 147 | 148 | let keyMap = { [48]: [ 0, `HELLO` ] }; 149 | 150 | let input = new KeyboardInput({ keyMap }); 151 | let keyboard = keysim.Keyboard.US_ENGLISH; 152 | 153 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForCharCode(48), document.body, true, keysim.KeyEvents.DOWN); 154 | 155 | input.pollInputs(); 156 | 157 | expect(input.getState(0, `HELLO`)).to.be.true; 158 | 159 | }); 160 | 161 | it(`should release any key that isn't used anymore`, () => { 162 | 163 | let keyMap1 = { [48]: [ 0, `A` ], [49]: [ 0, `B` ] }; 164 | let keyMap2 = { [48]: [ 0, `A` ] }; 165 | 166 | let input = new KeyboardInput({ keyMap: keyMap1 }); 167 | let keyboard = keysim.Keyboard.US_ENGLISH; 168 | 169 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForCharCode(48), document.body, true, keysim.KeyEvents.DOWN); 170 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForCharCode(49), document.body, true, keysim.KeyEvents.DOWN); 171 | 172 | input.pollInputs(); 173 | 174 | expect(input.getState(0, `A`)).to.be.true; 175 | expect(input.getState(0, `B`)).to.be.true; 176 | 177 | input.setKeyMap(keyMap2); 178 | input.pollInputs(); 179 | 180 | expect(input.getState(0, `A`)).to.be.true; 181 | expect(input.getState(0, `B`)).to.be.false; 182 | 183 | }); 184 | 185 | it(`should release any key that isn't mapped to the same input anymore`, () => { 186 | 187 | let keyMap1 = { [48]: [ 0, `A` ], [49]: [ 0, `B` ] }; 188 | let keyMap2 = { [48]: [ 0, `A` ], [49]: [ 0, `C` ] }; 189 | 190 | let input = new KeyboardInput({ keyMap: keyMap1 }); 191 | let keyboard = keysim.Keyboard.US_ENGLISH; 192 | 193 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForCharCode(48), document.body, true, keysim.KeyEvents.DOWN); 194 | keyboard.dispatchEventsForKeystroke(keyboard.keystrokeForCharCode(49), document.body, true, keysim.KeyEvents.DOWN); 195 | 196 | input.pollInputs(); 197 | 198 | expect(input.getState(0, `A`)).to.be.true; 199 | expect(input.getState(0, `B`)).to.be.true; 200 | 201 | input.setKeyMap(keyMap2); 202 | input.pollInputs(); 203 | 204 | expect(input.getState(0, `A`)).to.be.true; 205 | expect(input.getState(0, `B`)).to.be.false; 206 | expect(input.getState(0, `C`)).to.be.false; 207 | 208 | }); 209 | 210 | }); 211 | 212 | }); 213 | -------------------------------------------------------------------------------- /sources/devices/inputs/ManualInput.js: -------------------------------------------------------------------------------- 1 | export class ManualInput { 2 | 3 | /** 4 | * A ManualInput is an input device that will transmit any state you manually set from a Javascript API. It's a strictly better {@link NullInput}, that works on any environment while still giving you a way to trigger some events when you need to. 5 | * 6 | * @constructor 7 | * @implements {Input} 8 | * 9 | * @param {object} [options] - The device options. 10 | * @param {object} [options.codeMap] - The initial code map. 11 | */ 12 | 13 | constructor({ codeMap = null } = {}) { 14 | 15 | /** 16 | * This value contains the current code map used to translate codes. 17 | * 18 | * @member 19 | * @readonly 20 | * @type {object} 21 | */ 22 | 23 | this.codeMap = null; 24 | 25 | this.devices = {}; 26 | this.pending = {}; 27 | 28 | this.setCodeMap(codeMap); 29 | 30 | } 31 | 32 | /** 33 | * Set the code map that will be used to translate input codes from one to another. 34 | * 35 | * @param {object} codeMap - The new input map. 36 | */ 37 | 38 | setCodeMap(codeMap) { 39 | 40 | if (codeMap === this.codeMap) 41 | return; 42 | 43 | this.codeMap = codeMap; 44 | 45 | } 46 | 47 | /** 48 | * Set an input slot as being pressed. 49 | * 50 | * @param {number} port - The input slot controller port. 51 | * @param {number} code - The input slot code. 52 | */ 53 | 54 | down(port, code) { 55 | 56 | if (this.codeMap) 57 | code = this.codeMap[code]; 58 | 59 | this.pending[port] = this.pending[port] || {}; 60 | this.pending[port][code] = true; 61 | 62 | } 63 | 64 | /** 65 | * Set an input slot as being released. 66 | * 67 | * @param {number} port - The input slot controller port. 68 | * @param {number} code - The input slot code. 69 | */ 70 | 71 | up(port, code) { 72 | 73 | if (this.codeMap) 74 | code = this.codeMap[code]; 75 | 76 | this.pending[port] = this.pending[port] || {}; 77 | this.pending[port][code] = false; 78 | 79 | } 80 | 81 | pollInputs() { 82 | 83 | let pending = this.pending; 84 | this.pending = {}; 85 | 86 | for (let port of Reflect.ownKeys(pending)) { 87 | for (let code of Reflect.ownKeys(pending[port])) { 88 | this.devices[port] = this.devices[port] || {}; 89 | this.devices[port][code] = pending[port][code]; 90 | } 91 | } 92 | 93 | } 94 | 95 | getState(port, code) { 96 | 97 | return Boolean(this.devices[port] && this.devices[port][code]); 98 | 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /sources/devices/inputs/ManualInput.test.js: -------------------------------------------------------------------------------- 1 | import { ManualInput } from 'virtjs/devices/inputs/ManualInput'; 2 | 3 | describe(`ManualInput`, () => { 4 | 5 | describe(`#down()`, () => { 6 | 7 | it(`should set the corresponding key to "pressed"`, () => { 8 | 9 | let input = new ManualInput(); 10 | 11 | input.down(0, 0); 12 | input.pollInputs(); 13 | 14 | expect(input.getState(0, 0)).to.be.true; 15 | 16 | }); 17 | 18 | }); 19 | 20 | describe(`#up()`, () => { 21 | 22 | it(`should set the corresponding key to "released"`, () => { 23 | 24 | let input = new ManualInput(); 25 | 26 | input.down(0, 0); 27 | input.pollInputs(); 28 | 29 | expect(input.getState(0, 0)).to.be.true; 30 | 31 | input.up(0, 0); 32 | input.pollInputs(); 33 | 34 | expect(input.getState(0, 0)).to.be.false; 35 | 36 | }); 37 | 38 | }); 39 | 40 | describe(`#pollInputs()`, () => { 41 | 42 | it(`should send the pending inputs into the current inputs`, () => { 43 | 44 | let input = new ManualInput(); 45 | 46 | expect(input.getState(0, 0)).to.be.false; 47 | 48 | input.down(0, 0); 49 | 50 | expect(input.getState(0, 0)).to.be.false; 51 | 52 | input.pollInputs(); 53 | 54 | expect(input.getState(0, 0)).to.be.true; 55 | 56 | input.up(0, 0); 57 | 58 | expect(input.getState(0, 0)).to.be.true; 59 | 60 | input.pollInputs(); 61 | 62 | expect(input.getState(0, 0)).to.be.false; 63 | 64 | }); 65 | 66 | }); 67 | 68 | describe(`#setCodeMap()`, () => { 69 | 70 | it(`should set a translation map`, () => { 71 | 72 | let codeMap = { [`LEFT`]: 42 }; 73 | 74 | let input = new ManualInput(); 75 | input.setCodeMap(codeMap); 76 | 77 | input.down(0, `LEFT`); 78 | input.pollInputs(); 79 | 80 | expect(input.getState(0, 42)).to.be.true; 81 | 82 | }); 83 | 84 | it(`should be available right from the constructor`, () => { 85 | 86 | let codeMap = { [`LEFT`]: 42 }; 87 | let input = new ManualInput({ codeMap }); 88 | 89 | input.down(0, `LEFT`); 90 | input.pollInputs(); 91 | 92 | expect(input.getState(0, 42)).to.be.true; 93 | 94 | }); 95 | 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /sources/devices/inputs/NullInput.js: -------------------------------------------------------------------------------- 1 | export class NullInput { 2 | 3 | /** 4 | * A NullInput is an input device that will never transmit any key as pressed. Even if you don't want any fancy keyboard support or similar, {@link ManualInput} is probably a better candidate than NullInput since the later allows you to programmatically trigger key events should you need to, whereas NullInput will never ever do anything. 5 | * 6 | * @constructor 7 | * @implements {Input} 8 | */ 9 | 10 | constructor() { // eslint-disable-line no-useless-constructor 11 | 12 | // nothing 13 | 14 | } 15 | 16 | pollInputs() { 17 | 18 | // nothing 19 | 20 | } 21 | 22 | getState(port, inputCode) { 23 | 24 | return false; 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /sources/devices/screens/NullScreen.js: -------------------------------------------------------------------------------- 1 | export class NullScreen { 2 | 3 | /** 4 | * A NullScreen is a screen device that doesn't actually render anything. It might be useful if you want to run an engine on Node. 5 | * 6 | * @constructor 7 | * @implements {Screen} 8 | * 9 | * @param {object} [options] - The screen options. 10 | * @param {function} [options.flushCallback] - A callback that will be called when the screen will be flushed. 11 | */ 12 | 13 | constructor({ flushCallback = () => {} } = {}) { 14 | 15 | this.inputWidth = 0; 16 | this.inputHeight = 0; 17 | this.inputPitch = 0; 18 | 19 | this.inputFormat = null; 20 | this.inputData = null; 21 | 22 | this.outputWidth = 0; 23 | this.outputHeight = 0; 24 | 25 | this.flushCallback = flushCallback; 26 | 27 | } 28 | 29 | validateInputFormat(format) { 30 | 31 | return true; 32 | 33 | } 34 | 35 | setInputFormat(format) { 36 | 37 | this.inputFormat = format; 38 | 39 | } 40 | 41 | setInputSize(width, height, pitch = width) { 42 | 43 | this.inputWidth = width; 44 | this.inputHeight = height; 45 | this.inputPitch = pitch; 46 | 47 | } 48 | 49 | setInputData(data) { 50 | 51 | this.inputData = data; 52 | 53 | } 54 | 55 | setOutputSize(width, height) { 56 | 57 | this.outputWidth = width; 58 | this.outputHeight = height; 59 | 60 | } 61 | 62 | flushScreen() { 63 | 64 | this.flushCallback(this); 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /sources/devices/screens/Screen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Screen 3 | * @interface 4 | */ 5 | 6 | /** 7 | * This value contains the width of the input that the screen is expecting to render. 8 | * 9 | * Use {@link Screen#setInputSize} to change it. 10 | * 11 | * @member 12 | * @readonly 13 | * @name Screen#inputWidth 14 | * @type {number} 15 | */ 16 | 17 | /** 18 | * This value contains the height of the input that the screen is expecting to render. 19 | * 20 | * Use {@link Screen#setInputSize} to change it. 21 | * 22 | * @member 23 | * @readonly 24 | * @name Screen#inputHeight 25 | * @type {number} 26 | */ 27 | 28 | /** 29 | * This value contains the pitch of the input that the screen is expecting to render. 30 | * 31 | * The pitch is the actual amount of data in a pixel row. Some engines add extra data after each row in order to align the data size. 32 | * 33 | * Use {@link Screen#setInputSize} to change it. 34 | * 35 | * @member 36 | * @readonly 37 | * @name Screen#inputPitch 38 | * @type {number} 39 | */ 40 | 41 | /** 42 | * This value contains the input format that the screen is expecting to render. 43 | * 44 | * Use {@link Screen#setInputFormat} to change it. 45 | * 46 | * @member 47 | * @readonly 48 | * @name Screen#inputFormat 49 | * @type {ScreenInputFormat} 50 | */ 51 | 52 | /** 53 | * This value contains the data that the screen is currently rendering. 54 | * 55 | * Use {@link Screen#setInputData} to change it. 56 | * 57 | * @member 58 | * @readonly 59 | * @name Screen#inputData 60 | * @type {*} 61 | */ 62 | 63 | /** 64 | * This value contains the output width of the rendered data. 65 | * 66 | * Use {@link Screen#setOutputSize} to change it. 67 | * 68 | * @member 69 | * @readonly 70 | * @name Screen#outputWidth 71 | * @type {number} 72 | */ 73 | 74 | /** 75 | * This value contains the output height of the rendered data. 76 | * 77 | * Use {@link Screen#setOutputSize} to change it. 78 | * 79 | * @member 80 | * @readonly 81 | * @name Screen#outputHeight 82 | * @type {number} 83 | */ 84 | 85 | /** 86 | * An engine will call this function to inform the device of the new input size. 87 | * 88 | * @method 89 | * @name Screen#setInputSize 90 | * 91 | * @param {number} width - The new input width. 92 | * @param {number} height - The new input height. 93 | * @param {number} [pitch] - The new input pitch. 94 | */ 95 | 96 | /** 97 | * An engine will call this function to check if the device supports the specified input format. 98 | * 99 | * Return true if the screen device supports the specified input format. 100 | * 101 | * @method 102 | * @name Screen#validateInputFormat 103 | * 104 | * @param {ScreenInputFormat} format - The input format to validate. 105 | * 106 | * @return {bool} 107 | */ 108 | 109 | /** 110 | * An engine will call this function to inform the device of the new input format. 111 | * 112 | * Throw an exception if the screen device doesn't support the new input format. 113 | * 114 | * @method 115 | * @name Screen#setInputFormat 116 | * 117 | * @param {ScreenInputFormat} format - The new input format. 118 | */ 119 | 120 | /** 121 | * An engine will call this function to inform the device of the new input data. 122 | * 123 | * @method 124 | * @name Screen#setInputData 125 | * 126 | * @param {*} data - The new input data. 127 | */ 128 | 129 | /** 130 | * Change the output size. 131 | * 132 | * @method 133 | * @name Screen#setOutputSize 134 | * 135 | * @param {number} width - The new output width. 136 | * @param {number} height - The new output height. 137 | */ 138 | 139 | /** 140 | * Render the input data on the screen. 141 | * 142 | * @method 143 | * @name Screen#flushScreen 144 | */ 145 | -------------------------------------------------------------------------------- /sources/devices/screens/Screen.test.js: -------------------------------------------------------------------------------- 1 | import glContext from 'gl'; 2 | 3 | import { NullScreen } from 'virtjs/devices/screens/NullScreen'; 4 | import { WebGLScreen } from 'virtjs/devices/screens/WebGLScreen'; 5 | 6 | class HeadlessWebGLScreen extends WebGLScreen { 7 | 8 | constructor(options = {}) { 9 | 10 | super(Object.assign(options, { 11 | canvas: null, 12 | glBuilder: options => glContext(256, 256, options) 13 | })); 14 | 15 | } 16 | 17 | } 18 | 19 | describe(`Screen`, () => { 20 | 21 | let screens = { NullScreen, WebGLScreen: HeadlessWebGLScreen }; 22 | 23 | for (let [ name, Screen ] of Object.entries(screens)) { 24 | 25 | describe(name, () => { 26 | 27 | describe(`#setInputSize()`, () => { 28 | 29 | it(`should exist`, () => { 30 | 31 | let screen = new Screen(); 32 | 33 | expect(screen).to.have.property(`setInputSize`); 34 | expect(screen.setInputSize).to.be.a(`function`); 35 | 36 | }); 37 | 38 | it(`should set the #inputWidth, #inputHeight, and #inputPitch members`, () => { 39 | 40 | let screen = new Screen(); 41 | 42 | screen.setInputSize(256, 256, 1024); 43 | 44 | expect(screen.inputWidth).to.equal(256); 45 | expect(screen.inputHeight).to.equal(256); 46 | expect(screen.inputPitch).to.equal(1024); 47 | 48 | screen.setInputSize(512, 512, 2048); 49 | 50 | expect(screen.inputWidth).to.equal(512); 51 | expect(screen.inputHeight).to.equal(512); 52 | expect(screen.inputPitch).to.equal(2048); 53 | 54 | }); 55 | 56 | it(`should automatically compute the pitch if missing`, () => { 57 | 58 | let screen = new Screen(); 59 | 60 | screen.setInputSize(256, 256); 61 | 62 | expect(screen.inputWidth).to.equal(256); 63 | expect(screen.inputHeight).to.equal(256); 64 | expect(screen.inputPitch).to.equal(256); 65 | 66 | screen.setInputSize(512, 512); 67 | 68 | expect(screen.inputWidth).to.equal(512); 69 | expect(screen.inputHeight).to.equal(512); 70 | expect(screen.inputPitch).to.equal(512); 71 | 72 | }); 73 | 74 | }); 75 | 76 | describe(`#validateInputFormat()`, () => { 77 | 78 | it(`should exist`, () => { 79 | 80 | let screen = new Screen(); 81 | 82 | expect(screen).to.have.property(`validateInputFormat`); 83 | expect(screen.validateInputFormat).to.be.a(`function`); 84 | 85 | }); 86 | 87 | it(`should return a boolean`, () => { 88 | 89 | let screen = new Screen(); 90 | let inputFormat = { depth: 16, rMask: 0x00000000, gMask: 0x00000000, bMask: 0x00000000 }; 91 | 92 | expect(screen.validateInputFormat(inputFormat)).to.be.a(`boolean`); 93 | 94 | }); 95 | 96 | }); 97 | 98 | describe(`#setInputFormat()`, () => { 99 | 100 | it(`should exist`, () => { 101 | 102 | let screen = new Screen(); 103 | 104 | expect(screen).to.have.property(`setInputFormat`); 105 | expect(screen.setInputFormat).to.be.a(`function`); 106 | 107 | }); 108 | 109 | }); 110 | 111 | describe(`#setInputData()`, () => { 112 | 113 | it(`should exist`, () => { 114 | 115 | let screen = new Screen(); 116 | 117 | expect(screen).to.have.property(`setInputData`); 118 | expect(screen.setInputData).to.be.a(`function`); 119 | 120 | }); 121 | 122 | it(`should set the #inputData member`, () => { 123 | 124 | let screen = new Screen(); 125 | let inputData = new Uint8Array(10); 126 | 127 | expect(screen.inputData).to.be.null; 128 | 129 | screen.setInputData(inputData); 130 | 131 | expect(screen.inputData).to.equal(inputData); 132 | 133 | }); 134 | 135 | }); 136 | 137 | describe(`#setOutputSize()`, () => { 138 | 139 | it(`should exist`, () => { 140 | 141 | let screen = new Screen(); 142 | 143 | expect(screen).to.have.property(`setOutputSize`); 144 | expect(screen.setOutputSize).to.be.a(`function`); 145 | 146 | }); 147 | 148 | it(`should set the #outputWidth and #outputHeight members`, () => { 149 | 150 | let screen = new Screen(); 151 | 152 | screen.setOutputSize(256, 256); 153 | 154 | expect(screen.outputWidth).to.equal(256); 155 | expect(screen.outputHeight).to.equal(256); 156 | 157 | screen.setOutputSize(512, 512); 158 | 159 | expect(screen.outputWidth).to.equal(512); 160 | expect(screen.outputHeight).to.equal(512); 161 | 162 | }); 163 | 164 | }); 165 | 166 | describe(`#flushScreen()`, () => { 167 | 168 | it(`should exist`, () => { 169 | 170 | let screen = new Screen(); 171 | 172 | expect(screen).to.have.property(`flushScreen`); 173 | expect(screen.flushScreen).to.be.a(`function`); 174 | 175 | }); 176 | 177 | }); 178 | 179 | }); 180 | 181 | } 182 | 183 | }); 184 | -------------------------------------------------------------------------------- /sources/devices/screens/WebGLScreen.js: -------------------------------------------------------------------------------- 1 | let BITS_PER_BYTE = 8; 2 | let RENDER_DEPTH = 100; 3 | 4 | let TYPED_VIEW = Symbol(); 5 | let GL_FORMAT = Symbol(); 6 | let GL_TYPE = Symbol(); 7 | 8 | let gWebGlSupportedInputFormats = [ { 9 | 10 | depth: 16, 11 | 12 | rMask: 0b1111100000000000, 13 | gMask: 0b0000011111100000, 14 | bMask: 0b0000000000011111, 15 | aMask: 0b0000000000000000, 16 | 17 | [TYPED_VIEW]: Uint16Array, 18 | [GL_FORMAT]: `RGB`, 19 | [GL_TYPE]: `UNSIGNED_SHORT_5_6_5` 20 | 21 | } ]; 22 | 23 | let gVertexShaderScript = ` 24 | 25 | precision mediump float; 26 | 27 | uniform mat4 uMatrix; 28 | 29 | attribute vec3 aVertexPosition; 30 | attribute vec2 aVertexTextureUv; 31 | 32 | varying vec2 vTextureCoordinates; 33 | 34 | void main( void ) { 35 | 36 | vTextureCoordinates = vec2( aVertexTextureUv.s, 1.0 - aVertexTextureUv.t ); 37 | 38 | gl_Position = uMatrix * vec4( aVertexPosition, 1.0 ); 39 | 40 | } 41 | 42 | `; 43 | 44 | let gFragmentShaderScript = ` 45 | 46 | precision mediump float; 47 | 48 | uniform sampler2D uScreenTexture; 49 | 50 | varying vec2 vTextureCoordinates; 51 | 52 | void main( void ) { 53 | 54 | gl_FragColor = texture2D( uScreenTexture, vTextureCoordinates ); 55 | 56 | } 57 | 58 | `; 59 | 60 | function getMatchingInputFormat({ depth, rMask, gMask, bMask, aMask }) { 61 | 62 | for (let supported of gWebGlSupportedInputFormats) 63 | if (depth === supported.depth && rMask === supported.rMask && gMask === supported.gMask && bMask === supported.bMask && aMask === supported.aMask) 64 | return supported; 65 | 66 | return null; 67 | 68 | } 69 | 70 | function makeCanvasGlBuilder(canvas, options) { 71 | 72 | return () => { 73 | return canvas.getContext(`webgl`, options) || canvas.getContext(`experimental-webgl`, options); 74 | }; 75 | 76 | } 77 | 78 | export class WebGLScreen { 79 | 80 | /** 81 | * A WebGLScreen is a screen device that uses a WebGL canvas as rendering target. Note that you can also use a headless-gl instance as rendering context, in which case you can simply pass null as canvas parameter. 82 | * 83 | * @constructor 84 | * @implements {Screen} 85 | * 86 | * @param {object} [options] - The screen options. 87 | * @param {CanvasElement} [options.canvas] - The target canvas. 88 | * @param {object} [options.glOptions] - The extra option used to setup the WebGL context. 89 | * @param {function} [options.glBuilder] - A factory that will build the WebGL context. 90 | */ 91 | 92 | constructor({ canvas = document.createElement(`canvas`), glOptions = null, glBuilder = makeCanvasGlBuilder(canvas, glOptions) } = { }) { 93 | 94 | /** 95 | * The target canvas on which will be rendered the input data. 96 | * 97 | * @member 98 | * @readonly 99 | * @type {CanvasElement} 100 | */ 101 | 102 | this.canvas = canvas; 103 | 104 | /** 105 | * The WebGL context used to render the input data. 106 | * 107 | * @member 108 | * @readonly 109 | * @type {WebGLRenderingContext} 110 | */ 111 | 112 | this.gl = null; 113 | 114 | this.inputWidth = 0; 115 | this.inputHeight = 0; 116 | this.inputPitch = 0; 117 | 118 | this.inputFormat = null; 119 | this.inputData = null; 120 | 121 | this.outputWidth = 0; 122 | this.outputHeight = 0; 123 | 124 | this.pitchedInputData = null; 125 | 126 | this.shaderProgram = null; 127 | 128 | this.uMatrixLocation = null; 129 | this.uScreenTextureLocation = null; 130 | this.uInputResolutionLocation = null; 131 | this.uOutputResolutionLocation = null; 132 | 133 | this.aVertexPositionLocation = null; 134 | this.aVertexTextureUvLocation = null; 135 | 136 | this.textureIndex = 0; 137 | this.setupGl(glBuilder); 138 | 139 | let boundingBox = this.canvas && this.canvas.getBoundingClientRect(); 140 | let { width, height } = boundingBox || { width: 100, height: 100 }; 141 | 142 | this.setInputSize(Math.ceil(width), Math.ceil(height)); 143 | this.setOutputSize(Math.ceil(width), Math.ceil(height)); 144 | 145 | } 146 | 147 | setInputSize(width, height, pitch = width) { 148 | 149 | if (width === null && height === null) 150 | throw new Error(`Input width, height, and pitch cannot be null`); 151 | 152 | if (width === this.inputWidth && height === this.inputHeight && pitch === this.inputPitch) 153 | return; 154 | 155 | this.inputWidth = width; 156 | this.inputHeight = height; 157 | this.inputPitch = pitch; 158 | 159 | this.setupAlignmentBuffer(); 160 | 161 | this.updateViewport(); 162 | this.draw(); 163 | 164 | } 165 | 166 | setOutputSize(width = null, height = null) { 167 | 168 | if (width === this.outputWidth && height === this.outputHeight) 169 | return; 170 | 171 | this.outputWidth = width; 172 | this.outputHeight = height; 173 | 174 | this.updateViewport(); 175 | this.draw(); 176 | 177 | } 178 | 179 | setShaderProgram(shaderProgram) { 180 | 181 | if (shaderProgram === this.shaderProgram) 182 | return; 183 | 184 | if (this.shaderProgram !== null) 185 | this.gl.deleteProgram(this.shaderProgram); 186 | 187 | this.shaderProgram = shaderProgram; 188 | this.gl.useProgram(shaderProgram); 189 | 190 | this.uMatrixLocation = this.gl.getUniformLocation(shaderProgram, `uMatrix`); 191 | 192 | this.uScreenTextureLocation = this.gl.getUniformLocation(shaderProgram, `uScreenTexture`); 193 | this.gl.uniform1i(this.uScreenTextureLocation, 0); 194 | 195 | this.uInputResolutionLocation = this.gl.getUniformLocation(shaderProgram, `uInputResolution`); 196 | this.uOutputResolutionLocation = this.gl.getUniformLocation(shaderProgram, `uOutputResolution`); 197 | this.uViewportResolutionLocation = this.gl.getUniformLocation(shaderProgram, `uViewportResolution`); 198 | 199 | this.aVertexPositionLocation = this.gl.getAttribLocation(shaderProgram, `aVertexPosition`); 200 | this.gl.enableVertexAttribArray(this.aVertexPositionLocation); 201 | 202 | this.aVertexTextureUvLocation = this.gl.getAttribLocation(shaderProgram, `aVertexTextureUv`); 203 | this.gl.enableVertexAttribArray(this.aVertexTextureUvLocation); 204 | 205 | this.gl.bindBuffer(this.vertexPositionBuffer.bufferTarget, this.vertexPositionBuffer); 206 | this.gl.vertexAttribPointer(this.aVertexPositionLocation, this.vertexPositionBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 207 | 208 | this.gl.bindBuffer(this.vertexTextureUvBuffer.bufferTarget, this.vertexTextureUvBuffer); 209 | this.gl.vertexAttribPointer(this.aVertexTextureUvLocation, this.vertexTextureUvBuffer.itemSize, this.gl.FLOAT, false, 0, 0); 210 | 211 | } 212 | 213 | validateInputFormat(format) { 214 | 215 | return getMatchingInputFormat(format) !== null; 216 | 217 | } 218 | 219 | setInputFormat(partialFormat) { 220 | 221 | let fullFormat = getMatchingInputFormat(partialFormat); 222 | 223 | if (!fullFormat) 224 | throw new Error(`Invalid input format`); 225 | 226 | this.inputFormat = fullFormat; 227 | 228 | this.setupAlignmentBuffer(); 229 | 230 | } 231 | 232 | setInputData(data) { 233 | 234 | if (!data) 235 | return; 236 | 237 | this.inputData = data; 238 | 239 | } 240 | 241 | flushScreen() { 242 | 243 | this.draw(); 244 | 245 | } 246 | 247 | createTexture() { 248 | 249 | let texture = this.gl.createTexture(); 250 | 251 | return texture; 252 | 253 | } 254 | 255 | createBuffer(target, count, content) { 256 | 257 | let buffer = this.gl.createBuffer(); 258 | buffer.bufferTarget = target; 259 | buffer.itemCount = count; 260 | buffer.itemSize = content.length / count; 261 | 262 | this.gl.bindBuffer(buffer.bufferTarget, buffer); 263 | this.gl.bufferData(buffer.bufferTarget, content, this.gl.STATIC_DRAW); 264 | 265 | return buffer; 266 | 267 | } 268 | 269 | createOrthoMatrix(left, right, bottom, top, near, far) { 270 | 271 | let lr = 1 / (left - right); 272 | let bt = 1 / (bottom - top); 273 | let nf = 1 / (near - far); 274 | 275 | /* eslint-disable no-magic-numbers */ 276 | 277 | return [ -2 * lr, 0, 0, 0, 0, -2 * bt, 0, 0, 0, 0, 2 * nf, 0, (left + right) * lr, (bottom + top) * bt, (near + far) * nf, 1 ]; 278 | 279 | /* eslint-enable no-magic-numbers */ 280 | 281 | } 282 | 283 | setupBuffers() { 284 | 285 | /* eslint-disable no-magic-numbers */ 286 | 287 | this.vertexPositionBuffer = this.createBuffer(this.gl.ARRAY_BUFFER, 4, new Float32Array([ -1, -1, 0, /**/ 1, -1, 0, /**/ 1, 1, 0, /**/ -1, 1, 0 ])); 288 | this.vertexTextureUvBuffer = this.createBuffer(this.gl.ARRAY_BUFFER, 4, new Float32Array([ 0, 0, /**/ 1, 0, /**/ 1, 1, /**/ 0, 1 ])); 289 | this.vertexIndexBuffer = this.createBuffer(this.gl.ELEMENT_ARRAY_BUFFER, 4, new Uint16Array([ 0, 1, 3, 2 ])); 290 | 291 | /* eslint-enable no-magic-numbers */ 292 | 293 | } 294 | 295 | setupShaders() { 296 | 297 | this.fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, gFragmentShaderScript); 298 | this.vertexShader = this.createShader(this.gl.VERTEX_SHADER, gVertexShaderScript); 299 | this.linkShaders(this.fragmentShader, this.vertexShader); 300 | 301 | } 302 | 303 | setupTextures() { 304 | 305 | this.gl.activeTexture(this.gl.TEXTURE0); 306 | 307 | this.textures = [ this.createTexture(), this.createTexture() ]; 308 | 309 | this.textures.forEach(texture => { 310 | 311 | this.gl.bindTexture(this.gl.TEXTURE_2D, texture); 312 | 313 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST); 314 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST); 315 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); 316 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); 317 | 318 | }); 319 | 320 | } 321 | 322 | setupGl(glBuilder) { 323 | 324 | this.gl = glBuilder(); 325 | this.gl.clearColor(0, 0, 0, 0); 326 | 327 | this.setupBuffers(); 328 | this.setupShaders(); 329 | this.setupTextures(); 330 | 331 | } 332 | 333 | createShader(type, script) { 334 | 335 | let shader = this.gl.createShader(type); 336 | 337 | this.gl.shaderSource(shader, script); 338 | this.gl.compileShader(shader); 339 | 340 | if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) 341 | throw new Error(`Shader compilation failed: ${this.gl.getShaderInfoLog(shader)}`); 342 | 343 | return shader; 344 | 345 | } 346 | 347 | linkShaders(vertexShader, fragmentShader) { 348 | 349 | let shaderProgram = this.gl.createProgram(); 350 | 351 | this.gl.attachShader(shaderProgram, vertexShader); 352 | this.gl.attachShader(shaderProgram, fragmentShader); 353 | 354 | this.gl.linkProgram(shaderProgram); 355 | 356 | if (!this.gl.getProgramParameter(shaderProgram, this.gl.LINK_STATUS)) 357 | throw new Error(`Shader linking failed: ${this.gl.getError()}`); 358 | 359 | this.setShaderProgram(shaderProgram); 360 | 361 | } 362 | 363 | setupAlignmentBuffer() { 364 | 365 | if (!this.inputFormat) 366 | return; 367 | 368 | if (this.inputPitch === this.inputWidth * this.inputFormat.depth / BITS_PER_BYTE) { 369 | this.alignedData = null; 370 | } else { 371 | this.alignedData = new this.inputFormat[TYPED_VIEW](this.inputWidth * this.inputHeight); 372 | } 373 | 374 | } 375 | 376 | getAlignedData() { 377 | 378 | if (!this.alignedData) 379 | return this.inputData; 380 | 381 | let height = this.inputHeight; 382 | let byteLength = this.inputFormat.depth / BITS_PER_BYTE; 383 | 384 | let sourceRowSize = this.inputPitch / byteLength; 385 | let destinationRowSize = this.inputWidth; 386 | 387 | let source = this.inputData; 388 | let destination = this.alignedData; 389 | 390 | let sourceIndex = 0; 391 | let destinationIndex = 0; 392 | 393 | for (let y = 0; y < height; ++y) { 394 | 395 | for (let t = 0; t < destinationRowSize; ++t) 396 | destination[destinationIndex + t] = source[sourceIndex + t]; 397 | 398 | sourceIndex += sourceRowSize; 399 | destinationIndex += destinationRowSize; 400 | 401 | } 402 | 403 | return this.alignedData; 404 | 405 | } 406 | 407 | updateViewport() { 408 | 409 | let inputWidth = Math.max(1, this.inputWidth); 410 | let inputHeight = Math.max(1, this.inputHeight); 411 | 412 | let outputWidth = Math.max(1, this.outputWidth); 413 | let outputHeight = Math.max(1, this.outputHeight); 414 | 415 | if (outputWidth === null && outputHeight === null) { 416 | outputWidth = inputWidth; 417 | outputHeight = inputHeight; 418 | } 419 | 420 | if (outputWidth === null) 421 | outputWidth = inputWidth * (outputHeight / inputHeight); 422 | 423 | if (outputHeight === null) 424 | outputHeight = inputHeight * (outputWidth / inputWidth); 425 | 426 | let widthRatio = outputWidth / inputWidth; 427 | let heightRatio = outputHeight / inputHeight; 428 | 429 | let ratio = Math.min(widthRatio, heightRatio); 430 | 431 | let viewportWidth = widthRatio / ratio; 432 | let viewportHeight = heightRatio / ratio; 433 | 434 | if (this.canvas) { 435 | this.canvas.width = outputWidth; 436 | this.canvas.height = outputHeight; 437 | } 438 | 439 | if (this.gl.resize) 440 | this.gl.resize(outputWidth, outputHeight); 441 | 442 | let matrix = this.createOrthoMatrix(-viewportWidth, viewportWidth, -viewportHeight, viewportHeight, -RENDER_DEPTH, RENDER_DEPTH); 443 | this.gl.uniformMatrix4fv(this.uMatrixLocation, false, matrix); 444 | 445 | this.gl.uniform2f(this.uInputResolutionLocation, inputWidth, inputHeight); 446 | this.gl.uniform2f(this.uOutputResolutionLocation, outputWidth, outputHeight); 447 | this.gl.uniform2f(this.uViewportResolutionLocation, viewportWidth * inputWidth, viewportHeight * inputHeight); 448 | 449 | this.gl.viewport(0, 0, outputWidth, outputHeight); 450 | 451 | } 452 | 453 | draw() { 454 | 455 | this.gl.clear(this.gl.COLOR_BUFFER_BIT); 456 | 457 | if (!this.inputData || this.inputWidth === 0 || this.inputHeight === 0) 458 | return; 459 | 460 | let format = this.gl[this.inputFormat[GL_FORMAT]]; 461 | let type = this.gl[this.inputFormat[GL_TYPE]]; 462 | let data = this.getAlignedData(); 463 | 464 | let textureIndex = this.textureIndex++ % 2; 465 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[textureIndex]); 466 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGB, this.inputWidth, this.inputHeight, 0, format, type, data); 467 | 468 | this.gl.bindBuffer(this.vertexIndexBuffer.bufferTarget, this.vertexIndexBuffer); 469 | this.gl.drawElements(this.gl.TRIANGLE_STRIP, this.vertexIndexBuffer.itemCount, this.gl.UNSIGNED_SHORT, 0); 470 | 471 | } 472 | 473 | } 474 | -------------------------------------------------------------------------------- /sources/devices/timers/AnimationFrameTimer.js: -------------------------------------------------------------------------------- 1 | import { AsyncTimer } from './AsyncTimer'; 2 | 3 | export class AnimationFrameTimer extends AsyncTimer { 4 | 5 | /** 6 | * An AnimationFrameTimer is a timer device that makes use of the requestAnimationFrame/cancelAnimationFrame API from modern browsers to trigger asynchronous ticks. 7 | * 8 | * @constructor 9 | */ 10 | 11 | constructor() { // eslint-disable-line no-useless-constructor 12 | 13 | super(); 14 | 15 | } 16 | 17 | prepare(callback) { 18 | 19 | return window.requestAnimationFrame(callback); 20 | 21 | } 22 | 23 | cancel(animationFrameId) { 24 | 25 | window.cancelAnimationFrame(animationFrameId); 26 | 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /sources/devices/timers/AsyncTimer.js: -------------------------------------------------------------------------------- 1 | import { SerialTimer } from 'virtjs/devices/timers/SerialTimer'; 2 | import { makeFastTick } from 'virtjs/devices/timers/utils'; 3 | 4 | export class AsyncTimer { 5 | 6 | /** 7 | * An AsyncTimer is an asynchronous timer device. You can use it to run your emulator without blocking your main thread. However, unless you really want to implement a new asynchronous device on top of a new API, you're probably looking for {@link AnimationFrameTimer} for browser environments, or {@link ImmediateTimer} for Node.js environments. 8 | * 9 | * @constructor 10 | * @implements {Timer} 11 | * 12 | * @param {object} [options] - The timer options. 13 | * @param {function} [options.prepare] - The callback that will schedule the next cycle 14 | * @param {function} [options.cancel] - The callback that will abort the next cycle 15 | * 16 | * @see {@link AnimationFrameTimer} 17 | * @see {@link ImmediateTimer} 18 | */ 19 | 20 | constructor({ prepare, cancel } = { }) { 21 | 22 | if (prepare) 23 | this.prepare = prepare; 24 | 25 | if (cancel) 26 | this.cancel = cancel; 27 | 28 | this.running = false; 29 | this.nested = false; 30 | 31 | this.loopHandler = null; 32 | this.fastLoop = null; 33 | 34 | this.timer = new SerialTimer(); 35 | 36 | } 37 | 38 | nextTick(callback) { 39 | 40 | return this.timer.nextTick(callback); 41 | 42 | } 43 | 44 | cancelTick(handler) { 45 | 46 | return this.timer.cancelTick(handler); 47 | 48 | } 49 | 50 | start(beginning, ending) { 51 | 52 | if (this.running) 53 | throw new Error(`You can't start a timer that is already running`); 54 | 55 | if (this.nested) 56 | throw new Error(`You can't start a timer from its callbacks - use resume instead`); 57 | 58 | this.running = true; 59 | 60 | let resolve; 61 | let reject; 62 | 63 | let promise = new Promise((resolveFn, rejectFn) => { 64 | 65 | resolve = resolveFn; 66 | reject = rejectFn; 67 | 68 | }); 69 | 70 | let fastTick = makeFastTick(beginning, ending, () => { 71 | 72 | this.timer.one(); 73 | 74 | }); 75 | 76 | let mainLoop = () => { 77 | 78 | if (!this.running) { 79 | 80 | resolve(); 81 | 82 | } else try { 83 | 84 | this.prepare(mainLoop); 85 | 86 | this.nested = true; 87 | fastTick(); 88 | this.nested = false; 89 | 90 | } catch (e) { 91 | 92 | this.running = false; 93 | this.nested = false; 94 | 95 | reject(e); 96 | 97 | } 98 | 99 | }; 100 | 101 | this.prepare(mainLoop); 102 | 103 | return promise; 104 | 105 | } 106 | 107 | resume() { 108 | 109 | if (!this.nested) 110 | throw new Error(`You can't resume a timer from anywhere else than its callbacks - use start instead`); 111 | 112 | if (this.running) 113 | return; 114 | 115 | this.running = true; 116 | 117 | } 118 | 119 | stop() { 120 | 121 | if (!this.running) 122 | return; 123 | 124 | this.running = false; 125 | 126 | } 127 | 128 | /** 129 | * This method should be specialized, either via subclassing, or by passing the proper parameter when instanciating the timer. 130 | * 131 | * @protected 132 | * 133 | * @type {prepareCallback} 134 | */ 135 | 136 | prepare() { 137 | 138 | throw new Error(`Unimplemented`); 139 | 140 | } 141 | 142 | /** 143 | * This method should be specialized, either via subclassing, or by passing the proper parameter when instanciating the timer. 144 | * 145 | * @protected 146 | * 147 | * @type {cancelCallback} 148 | */ 149 | 150 | cancel() { 151 | 152 | throw new Error(`Unimplemented`); 153 | 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /sources/devices/timers/AsyncTimer.test.js: -------------------------------------------------------------------------------- 1 | import { ImmediateTimer } from 'virtjs/devices/timers/ImmediateTimer'; 2 | 3 | describe(`AsyncTimer`, () => { 4 | 5 | describe(`execution flow`, () => { 6 | 7 | it(`should not call registered ticks if stopped before even starting`, () => { 8 | 9 | let timer = new ImmediateTimer(); 10 | let tick = chai.spy(() => {}); 11 | 12 | timer.nextTick(tick); 13 | 14 | let promise = timer.start(); 15 | timer.stop(); 16 | 17 | return promise.then(() => { 18 | expect(tick).to.not.have.been.called(); 19 | }); 20 | 21 | }); 22 | 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /sources/devices/timers/ImmediateTimer.js: -------------------------------------------------------------------------------- 1 | import { AsyncTimer } from './AsyncTimer'; 2 | 3 | export class ImmediateTimer extends AsyncTimer { 4 | 5 | /** 6 | * An ImmediateTimer is a timer device that makes use of the setImmediate/clearImmediate API from Node to trigger aynchronous ticks. 7 | * 8 | * @constructor 9 | */ 10 | 11 | constructor() { // eslint-disable-line no-useless-constructor 12 | 13 | super(); 14 | 15 | } 16 | 17 | prepare(callback) { 18 | 19 | return setImmediate(callback); 20 | 21 | } 22 | 23 | cancel(handler) { 24 | 25 | clearImmediate(handler); 26 | 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /sources/devices/timers/NullTimer.js: -------------------------------------------------------------------------------- 1 | export class NullTimer { 2 | 3 | /** 4 | * A NullTimer is a timer device that will never tick. You probably don't want to use it. If you're looking for a synchronous timer, check {@link SerialTimer} instead. If you're looking for an asynchronous timer that works on Node.js, check {@link ImmediateTimer} instead. If you're looking for a synchronous timer that works on Node.js, check {@link SerialTimer} instead. 5 | * 6 | * @constructor 7 | * @implements {Timer} 8 | * 9 | * @see {@link SerialTimer} 10 | */ 11 | 12 | constructor() { // eslint-disable-line no-useless-constructor 13 | 14 | // nothing 15 | 16 | } 17 | 18 | nextTick(callback) { 19 | 20 | // nothing 21 | 22 | } 23 | 24 | cancelTick(nextTickId) { 25 | 26 | // nothing 27 | 28 | } 29 | 30 | start(beginning, ending) { 31 | 32 | return new Promise(() => {}); 33 | 34 | } 35 | 36 | stop() { 37 | 38 | // nothing 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /sources/devices/timers/SerialTimer.js: -------------------------------------------------------------------------------- 1 | import { makeFastTick } from 'virtjs/devices/timers/utils'; 2 | 3 | let HANDLER_FN_SIZE = 31; 4 | let HANDLER_FN_PATTERN = 0x7FFFFFFF; 5 | 6 | export class SerialTimer { 7 | 8 | /** 9 | * A SerialTimer is a synchronous timer device. You can use it to run your emulator synchronously (ie. blocking the main thread). It also has the ability to only run a finite number of ticks before returning, which is quite valuable when debugging engines. 10 | * 11 | * @constructor 12 | * @implements {Timer} 13 | */ 14 | 15 | constructor() { 16 | 17 | this.running = false; 18 | this.nested = false; 19 | 20 | this.queues = [ [ ], [ ] ]; 21 | this.activeQueueIndex = 0; 22 | 23 | } 24 | 25 | nextTick(callback) { 26 | 27 | let activeQueueIndex = this.activeQueueIndex; 28 | let queue = this.queues[activeQueueIndex]; 29 | let callbackIndex = queue.length; 30 | 31 | queue.push(callback); 32 | 33 | return activeQueueIndex << HANDLER_FN_SIZE | callbackIndex; 34 | 35 | } 36 | 37 | cancelTick(handler) { 38 | 39 | let activeQueueIndex = handler >>> HANDLER_FN_SIZE; 40 | let callbackIndex = handler & HANDLER_FN_PATTERN; 41 | 42 | this.queues[activeQueueIndex][callbackIndex] = null; 43 | 44 | } 45 | 46 | start(beginning, ending) { 47 | 48 | if (this.running) 49 | throw new Error(`You can't start a timer that is already running`); 50 | 51 | if (this.nested) 52 | throw new Error(`You can't start a timer from its callbacks - use resume instead`); 53 | 54 | let fastTick = makeFastTick(beginning, ending, () => { 55 | this.one(); 56 | }); 57 | 58 | this.running = true; 59 | this.nested = true; 60 | 61 | while (this.running) 62 | fastTick(); 63 | 64 | this.nested = false; 65 | 66 | } 67 | 68 | resume() { 69 | 70 | if (!this.nested) 71 | throw new Error(`You can't resume a timer from anywhere else than its callbacks - use start instead`); 72 | 73 | if (this.running) 74 | return; 75 | 76 | this.running = true; 77 | 78 | } 79 | 80 | stop() { 81 | 82 | if (!this.running) 83 | return; 84 | 85 | this.running = false; 86 | 87 | } 88 | 89 | /** 90 | * Start the emulator. Run a single cycle then exit. 91 | */ 92 | 93 | one() { 94 | 95 | let activeQueueIndex = this.activeQueueIndex; 96 | this.activeQueueIndex = activeQueueIndex ^ 1; 97 | 98 | let queue = this.queues[activeQueueIndex]; 99 | 100 | for (let t = 0, T = queue.length; t < T; ++t) 101 | queue[t] && queue[t](); 102 | 103 | queue.length = 0; 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /sources/devices/timers/SerialTimer.test.js: -------------------------------------------------------------------------------- 1 | import { SerialTimer } from 'virtjs/devices/timers/SerialTimer'; 2 | 3 | describe(`SerialTimer`, () => { 4 | 5 | describe(`execution flow`, () => { 6 | 7 | it(`should keep running until being stopped`, () => { 8 | 9 | let timer = new SerialTimer(); 10 | let counter = 0; 11 | 12 | let fn = () => { 13 | if (++counter === 10) { 14 | timer.stop(); 15 | } else { 16 | timer.nextTick(fn); 17 | } 18 | }; 19 | 20 | timer.nextTick(fn); 21 | timer.start(); 22 | 23 | expect(counter).to.equal(10); 24 | 25 | }); 26 | 27 | }); 28 | 29 | describe(`#one()`, () => { 30 | 31 | it(`should run a single iteration before returning`, () => { 32 | 33 | let timer = new SerialTimer(); 34 | 35 | let tick = chai.spy(() => {}); 36 | 37 | timer.nextTick(tick); 38 | timer.one(); 39 | 40 | expect(tick).to.have.been.called.once; 41 | 42 | }); 43 | 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /sources/devices/timers/TimeoutTimer.js: -------------------------------------------------------------------------------- 1 | import { AsyncTimer } from './AsyncTimer'; 2 | 3 | export class TimeoutTimer extends AsyncTimer { 4 | 5 | /** 6 | * A TimeoutTimer is a timer device that makes use of the setTimeout/clearTimeout API to trigger asynchronous ticks. 7 | * 8 | * @constructor 9 | * 10 | * @param {object} [options] - The timer options. 11 | * @param {number} [options.framesPerSecond] - The number of frames that should be triggered every second. 12 | */ 13 | 14 | constructor({ framesPerSecond = 60 } = {}) { // eslint-disable-line no-useless-constructor 15 | 16 | super(); 17 | 18 | this.framesPerSecond = 60; 19 | 20 | } 21 | 22 | prepare(callback) { 23 | 24 | return setTimeout(callback, 1000 / this.framesPerSecond); 25 | 26 | } 27 | 28 | cancel(timeoutId) { 29 | 30 | clearTimeout(timeoutId); 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sources/devices/timers/Timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Timer 3 | * @interface 4 | */ 5 | 6 | /** 7 | * An engine will call this function if it wants to schedule a function to be called at the next tick. 8 | * 9 | * @method 10 | * @name Timer#nextTick 11 | * 12 | * @param {function} fn - The function that should be registered. 13 | * @return {Opaque} handler 14 | */ 15 | 16 | /** 17 | * An engine will call this function if it wants to prevent a scheduled function from being executed. 18 | * 19 | * @method 20 | * @name Timer#cancelTick 21 | * 22 | * @param {Opaque} handler - The handler returned by {@link Timer#nextTick}. 23 | */ 24 | 25 | /** 26 | * Start the timer. 27 | * 28 | * @method 29 | * @name Timer#start 30 | * 31 | * @param {function} [beginning] - An optional function that will be called before each tick. 32 | * @param {function} [ending] - An optional function that will be caled after each tick. 33 | */ 34 | 35 | /** 36 | * Stop the timer. 37 | * 38 | * @method 39 | * @name Timer#stop 40 | */ 41 | -------------------------------------------------------------------------------- /sources/devices/timers/Timer.test.js: -------------------------------------------------------------------------------- 1 | import { ImmediateTimer } from 'virtjs/devices/timers/ImmediateTimer'; 2 | import { SerialTimer } from 'virtjs/devices/timers/SerialTimer'; 3 | 4 | describe(`Timer`, () => { 5 | 6 | let timers = { SerialTimer, AsyncTimer: ImmediateTimer }; 7 | 8 | for (let [ name, Timer ] of Object.entries(timers)) { 9 | 10 | describe(name, () => { 11 | 12 | describe(`execution flow`, () => { 13 | 14 | it(`should not call back a tick that hasn't registered itself again`, () => { 15 | 16 | let timer = new Timer(); 17 | let counter = 0; 18 | 19 | let tick = chai.spy(() => {}); 20 | timer.nextTick(tick); 21 | 22 | let ending = () => { if (++counter === 10) timer.stop(); }; 23 | 24 | return Promise.resolve(timer.start(null, ending)).then(() => { 25 | expect(tick).to.have.been.called.once; 26 | }); 27 | 28 | }); 29 | 30 | }); 31 | 32 | describe(`#nextTick()`, () => { 33 | 34 | it(`should exist`, () => { 35 | 36 | let timer = new Timer(); 37 | 38 | expect(timer).to.have.property(`nextTick`); 39 | expect(timer.nextTick).to.be.a(`function`); 40 | 41 | }); 42 | 43 | it(`should return an handler`, () => { 44 | 45 | let timer = new Timer(); 46 | 47 | expect(timer.nextTick(() => {})).to.exist; 48 | 49 | }); 50 | 51 | it(`should return unique handlers`, () => { 52 | 53 | let timer = new Timer(); 54 | let fn = () => {}; 55 | 56 | let handler1 = timer.nextTick(() => {}); 57 | let handler2 = timer.nextTick(() => {}); 58 | let handler3 = timer.nextTick(fn); 59 | let handler4 = timer.nextTick(fn); 60 | timer.cancelTick(handler4); 61 | let handler5 = timer.nextTick(fn); 62 | 63 | expect(handler1).to.not.be.oneOf([ handler2, handler3, handler4, handler5 ]); 64 | expect(handler2).to.not.be.oneOf([ handler1, handler3, handler4, handler5 ]); 65 | expect(handler3).to.not.be.oneOf([ handler1, handler2, handler4, handler5 ]); 66 | expect(handler4).to.not.be.oneOf([ handler1, handler2, handler3, handler5 ]); 67 | expect(handler5).to.not.be.oneOf([ handler1, handler2, handler3, handler4 ]); 68 | 69 | }); 70 | 71 | }); 72 | 73 | describe(`#cancelTick()`, () => { 74 | 75 | it(`should exist`, () => { 76 | 77 | let timer = new Timer(); 78 | 79 | expect(timer).to.have.property(`cancelTick`); 80 | expect(timer.cancelTick).to.be.a(`function`); 81 | 82 | }); 83 | 84 | it(`should cancel registered ticks`, () => { 85 | 86 | let timer = new Timer(); 87 | 88 | let tick1 = chai.spy(() => {}); 89 | let tick2 = chai.spy(() => {}); 90 | let tick3 = chai.spy(() => {}); 91 | 92 | let ending = () => { timer.stop(); }; 93 | 94 | let handler1 = timer.nextTick(tick1); 95 | let handler2 = timer.nextTick(tick2); 96 | let handler3 = timer.nextTick(tick3); 97 | 98 | timer.cancelTick(handler1); 99 | timer.cancelTick(handler3); 100 | 101 | return Promise.resolve(timer.start(null, ending)).then(() => { 102 | expect(tick1).to.not.have.been.called(); 103 | expect(tick2).to.have.been.called.once; 104 | expect(tick3).to.not.have.been.called(); 105 | }); 106 | 107 | }); 108 | 109 | }); 110 | 111 | describe(`#start()`, () => { 112 | 113 | it(`should exist`, () => { 114 | 115 | let timer = new Timer(); 116 | 117 | expect(timer).to.have.property(`start`); 118 | expect(timer.start).to.be.a(`function`); 119 | 120 | }); 121 | 122 | it(`should call "beginning" before each iteration`, () => { 123 | 124 | let timer = new Timer(); 125 | let counter = 0; 126 | 127 | let beginning = chai.spy(() => {}); 128 | 129 | timer.nextTick(function tick() { 130 | 131 | expect(beginning).to.have.been.called.exactly(counter + 1); 132 | 133 | if (++counter === 10) { 134 | timer.stop(); 135 | } else { 136 | timer.nextTick(tick); 137 | } 138 | 139 | }); 140 | 141 | expect(beginning).to.have.been.called.exactly(counter); 142 | 143 | return timer.start(beginning); 144 | 145 | }); 146 | 147 | it(`should call "ending" before each iteration`, () => { 148 | 149 | let timer = new Timer(); 150 | let counter = 0; 151 | 152 | let ending = chai.spy(() => {}); 153 | 154 | timer.nextTick(function tick() { 155 | 156 | expect(ending).to.have.been.called.exactly(counter); 157 | 158 | if (++counter === 10) { 159 | timer.stop(); 160 | } else { 161 | timer.nextTick(tick); 162 | } 163 | 164 | }); 165 | 166 | expect(ending).to.have.been.called.exactly(counter); 167 | 168 | return timer.start(null, ending); 169 | 170 | }); 171 | 172 | it(`should call both "beginning" and "ending" at the right time`, () => { 173 | 174 | let timer = new Timer(); 175 | let counter = 0; 176 | 177 | let beginning = chai.spy(() => {}); 178 | let ending = chai.spy(() => {}); 179 | 180 | timer.nextTick(function tick() { 181 | 182 | expect(beginning).to.have.been.called.exactly(counter + 1); 183 | expect(ending).to.have.been.called.exactly(counter); 184 | 185 | if (++counter === 10) { 186 | timer.stop(); 187 | } else { 188 | timer.nextTick(tick); 189 | } 190 | 191 | }); 192 | 193 | expect(beginning).to.have.been.called.exactly(counter); 194 | expect(ending).to.have.been.called.exactly(counter); 195 | 196 | return timer.start(beginning, ending); 197 | 198 | }); 199 | 200 | }); 201 | 202 | describe(`#resume()`, () => { 203 | 204 | it(`should exist`, () => { 205 | 206 | let timer = new Timer(); 207 | 208 | expect(timer).to.have.property(`resume`); 209 | expect(timer.resume).to.be.a(`function`); 210 | 211 | }); 212 | 213 | it(`should be able to resume from an early stop`, () => { 214 | 215 | let timer = new Timer(); 216 | let counter = 0; 217 | 218 | let beginning = () => { timer.stop(); }; 219 | let ending = () => { if (counter < 10) timer.resume(); }; 220 | 221 | timer.nextTick(function tick() { 222 | 223 | if (++counter === 10) { 224 | timer.stop(); 225 | } else { 226 | timer.nextTick(tick); 227 | } 228 | 229 | }); 230 | 231 | return Promise.resolve(timer.start(beginning, ending)).then(() => { 232 | expect(counter).to.equal(10); 233 | }); 234 | 235 | }); 236 | 237 | }); 238 | 239 | describe(`#stop()`, () => { 240 | 241 | it(`should exist`, () => { 242 | 243 | let timer = new Timer(); 244 | 245 | expect(timer).to.have.property(`stop`); 246 | expect(timer.stop).to.be.a(`function`); 247 | 248 | }); 249 | 250 | }); 251 | 252 | }); 253 | 254 | } 255 | 256 | }); 257 | -------------------------------------------------------------------------------- /sources/devices/timers/utils.js: -------------------------------------------------------------------------------- 1 | export function makeFastTick(beginning, ending, main) { 2 | 3 | /* eslint-disable no-nested-ternary */ 4 | 5 | return beginning && ending ? function fastTickBE() { 6 | 7 | beginning(); 8 | main(); 9 | ending(); 10 | 11 | } : beginning ? function fastTickB() { 12 | 13 | beginning(); 14 | main(); 15 | 16 | } : ending ? function fastTickE() { 17 | 18 | main(); 19 | ending(); 20 | 21 | } : main; 22 | 23 | /* eslint-enable no-nested-ternary */ 24 | 25 | } 26 | -------------------------------------------------------------------------------- /sources/index.js: -------------------------------------------------------------------------------- 1 | let req = require.context(`./`, true, /^(?!.*\.test\.js$).*\.js$/); 2 | 3 | for (let name of req.keys()) { 4 | 5 | let filtered = name.replace(/^.\/|\.js$/g, ``); 6 | 7 | if (filtered === `index`) 8 | continue; 9 | 10 | let module = req(name); 11 | let parts = filtered.split(`/`); 12 | 13 | let target = exports; 14 | 15 | for (let t = 0; t < parts.length - 1; ++t) 16 | target = target[parts[t]] = target[parts[t]] || {}; 17 | 18 | let main = module[parts[parts.length - 1]] || {}; 19 | target[parts[parts.length - 1]] = main; 20 | 21 | for (let symbol of Reflect.ownKeys(module)) { 22 | if (symbol !== parts[parts.length - 1]) { 23 | main[symbol] = module[symbol]; 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /sources/mixins/EmitterMixin.js: -------------------------------------------------------------------------------- 1 | let LISTENERS = Symbol(); 2 | 3 | export class EmitterMixin { 4 | 5 | constructor() { 6 | 7 | this[LISTENERS] = { [`*`]: [ ] }; 8 | 9 | } 10 | 11 | on(event, callback, context) { 12 | 13 | if (typeof this[LISTENERS][event] === `undefined`) 14 | this[LISTENERS][event] = [ ]; 15 | 16 | this[LISTENERS][event].push([ callback, context ]); 17 | 18 | } 19 | 20 | off(event, callback, context) { 21 | 22 | if (typeof this[LISTENERS][event] === `undefined`) 23 | return; 24 | 25 | let listeners = this[LISTENERS][event]; 26 | 27 | for (let t = 0, T = listeners.length; t < T; ++t) 28 | if (listeners[t][0] === callback && listeners[t][1] === context) 29 | break; 30 | 31 | listeners.splice(listeners.findIndex(([ lCallback, lContext ]) => { 32 | return lCallback === callback && lContext === context; 33 | }), 1); 34 | 35 | } 36 | 37 | emit(event, data) { 38 | 39 | if (typeof this[LISTENERS][event] === `undefined`) 40 | return; 41 | 42 | this[LISTENERS][event].forEach(([ callback, context ]) => { 43 | Reflect.apply(callback, context, [ data ]); 44 | }); 45 | 46 | this[LISTENERS][`*`].forEach(([ callback, context ]) => { 47 | Reflect.apply(callback, context, [ event, data ]); 48 | }); 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /sources/utils/DataUtils.js: -------------------------------------------------------------------------------- 1 | const base64DataUrl = /^data:[^;]*;base64,([a-zA-Z0-9+/]+={0,2})$/; 2 | 3 | export function areEqualBuffers(a, b) { 4 | 5 | if (a.byteLength !== b.byteLength) 6 | return false; 7 | 8 | return areEqualViews(new Uint8View(a), new Uint8View(b)); 9 | 10 | } 11 | 12 | export function areEqualViews(a, b) { 13 | 14 | if (a.length !== b.length) 15 | return false; 16 | 17 | for (let t = 0, T = a.length; t < T; ++t) 18 | if (a[t] !== b[t]) 19 | return false; 20 | 21 | return true; 22 | 23 | } 24 | 25 | export function nodeToUint8(... buffers) { 26 | 27 | let totalByteLength = buffers.reduce((sum, buffer) => sum + buffer.length, 0); 28 | 29 | let array = new Uint8Array(totalByteLength); 30 | let offset = 0; 31 | 32 | for (let buffer of buffers) { 33 | for (let t = 0, T = array.length; t < T; ++t) { 34 | array[offset++] = buffer[t]; 35 | } 36 | } 37 | 38 | return array; 39 | 40 | } 41 | 42 | export function binaryStringToUint8(... strings) { 43 | 44 | let totalByteLength = strings.reduce((sum, string) => sum + string.length, 0); 45 | 46 | let array = new Uint8Array(totalByteLength); 47 | let offset = 0; 48 | 49 | for (let string of strings) { 50 | for (let t = 0, T = string.length; t < T; ++t) { 51 | array[offset++] = string.charCodeAt(t); 52 | } 53 | } 54 | 55 | return array; 56 | 57 | } 58 | 59 | export function base64ToUint8(... strings) { 60 | 61 | let isBrowser = typeof window !== `undefined`; 62 | 63 | if (isBrowser) { 64 | return binaryStringToUint8(... strings.map(string => atob(string))); 65 | } else { 66 | return nodeToUint8(... strings.map(string => new Buffer(string, `base64`))); 67 | } 68 | 69 | } 70 | 71 | export function fetchArrayBuffer(path) { 72 | 73 | return new Promise((resolve, reject) => { 74 | 75 | let isBlob = typeof Blob !== `undefined` && path instanceof Blob; 76 | let isDataURI = typeof path === `string` && path.match(base64DataUrl); 77 | let isBrowser = typeof window !== `undefined`; 78 | let isWeb = isBrowser || /^(https?:\/\/|blob:)/.test(path); 79 | 80 | if (!isWeb && path.indexOf(`:`) !== -1) 81 | throw new Error(`Invalid protocol`); 82 | 83 | if (isBlob) { 84 | 85 | let fileReader = new FileReader(); 86 | 87 | fileReader.addEventListener(`load`, e => { 88 | resolve(e.target.result); 89 | }); 90 | 91 | fileReader.addEventListener(`error`, e => { 92 | reject(); 93 | }); 94 | 95 | fileReader.readAsArrayBuffer(path); 96 | 97 | } else if (isDataURI) { 98 | 99 | resolve(base64ToUint8(isDataURI[1]).buffer); 100 | 101 | } else if (isBrowser) { 102 | 103 | let xhr = new XMLHttpRequest(); 104 | 105 | xhr.open(`GET`, path, true); 106 | xhr.responseType = `arraybuffer`; 107 | 108 | xhr.onload = () => { 109 | resolve(xhr.response); 110 | }; 111 | 112 | xhr.onerror = () => { 113 | reject(xhr.status); 114 | }; 115 | 116 | xhr.send(null); 117 | 118 | } else if (isWeb) { 119 | 120 | let protocol = path.substr(0, path.indexOf(`:`)); 121 | let web = eval(`require(protocol /* http or https */)`); 122 | 123 | let buffers = [ ]; 124 | 125 | web.get(path, res => { 126 | 127 | res.on(`data`, chunk => { 128 | buffers.push(chunk); 129 | }); 130 | 131 | res.on(`error`, err => { 132 | reject(err.message); 133 | }); 134 | 135 | res.on(`end`, () => { 136 | resolve(nodeToUint8(... buffers).buffer); 137 | }); 138 | 139 | }); 140 | 141 | } else { 142 | 143 | let fs = eval(`require('fs')`); 144 | 145 | fs.readFile(path, (err, buffer) => { 146 | 147 | if (err) { 148 | reject(err); 149 | } else { 150 | resolve(nodeToUint8(buffer).buffer); 151 | } 152 | 153 | }); 154 | 155 | } 156 | 157 | }); 158 | 159 | } 160 | -------------------------------------------------------------------------------- /sources/utils/FormatUtils.js: -------------------------------------------------------------------------------- 1 | let BITS_PER_HEX_CHAR = 4; 2 | let HEX_BASE = 16; 3 | 4 | export function formatAddress(address, bits, withPrefix = true) { 5 | 6 | if (isNaN(address)) 7 | return `NaN`; 8 | 9 | let str = Number(address).toString(HEX_BASE).toLowerCase(); 10 | 11 | if (typeof bits !== `undefined`) 12 | while (str.length < Math.ceil(bits / BITS_PER_HEX_CHAR)) 13 | str = `0` + str; // eslint-disable-line prefer-template 14 | 15 | if (withPrefix) 16 | str = `$` + str; // eslint-disable-line prefer-template 17 | 18 | return str; 19 | 20 | } 21 | 22 | export function formatRelativeAddress(sourceAddress, relativeAddress, sourceBits, relativeBits) { 23 | 24 | let sign = relativeAddress < 0 ? `-` : `+`; 25 | 26 | if (typeof relativeAddress !== `string` || !isNaN(relativeAddress)) 27 | relativeAddress = formatAddress(Math.abs(relativeAddress), relativeBits, false); 28 | 29 | return formatAddress(sourceAddress, sourceBits) + sign + relativeAddress; 30 | 31 | } 32 | 33 | export function formatDecimal(value, size) { 34 | 35 | if (isNaN(value)) 36 | return `NaN`; 37 | 38 | let str = Number(value).toString(); 39 | 40 | if (typeof size === `undefined`) 41 | return str; 42 | 43 | for (let t = str.length; t < size; ++t) 44 | str = `0` + str; // eslint-disable-line prefer-template 45 | 46 | return str; 47 | 48 | } 49 | 50 | export function formatString(str, size, leftAligned = true) { 51 | 52 | str = str.toString(); 53 | 54 | if (typeof size === `undefined`) 55 | return str; 56 | 57 | for (let t = str.length; t < size; ++t) { 58 | if (leftAligned) { 59 | str = str + ` `; // eslint-disable-line prefer-template 60 | } else { 61 | str = ` ` + str; // eslint-disable-line prefer-template 62 | } 63 | } 64 | 65 | return str; 66 | 67 | } 68 | 69 | export function formatHexadecimal(value, bits, withPrefix = true) { 70 | 71 | if (isNaN(value)) 72 | return `NaN`; 73 | 74 | let str = Number(value).toString(HEX_BASE).toLowerCase(); 75 | 76 | if (typeof bits !== `undefined`) 77 | while (str.length < Math.ceil(bits / BITS_PER_HEX_CHAR)) 78 | str = `0` + str; // eslint-disable-line prefer-template 79 | 80 | if (withPrefix) 81 | str = `0x` + str; // eslint-disable-line prefer-template 82 | 83 | return str; 84 | 85 | } 86 | 87 | export function formatBinary(value, bits, withPrefix = true) { 88 | 89 | if (isNaN(value)) 90 | return `NaN`; 91 | 92 | let str = Number(value).toString(2); 93 | 94 | if (typeof bits !== `undefined`) 95 | while (str.length < bits) 96 | str = `0` + str; // eslint-disable-line prefer-template 97 | 98 | if (withPrefix) 99 | str = `0b` + str; // eslint-disable-line prefer-template 100 | 101 | return str; 102 | 103 | } 104 | -------------------------------------------------------------------------------- /sources/utils/MemoryUtils.js: -------------------------------------------------------------------------------- 1 | export function memset(destination, value, offset, size) { 2 | 3 | for (let t = 0; t < size; ++t) 4 | destination[offset + t] = value; 5 | 6 | return destination; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /sources/utils/ObjectUtils.js: -------------------------------------------------------------------------------- 1 | export function createDefensiveProxy(object) { 2 | 3 | if (typeof Proxy === `undefined`) { 4 | 5 | console.warn(`Proxies are not available in your browser, and have been turned off.`); // eslint-disable-line no-console 6 | 7 | return object; 8 | 9 | } else { 10 | 11 | console.warn(`Proxies are slows, and should not be enabled in production.`); // eslint-disable-line no-console 12 | 13 | return new Proxy(object, { 14 | 15 | get(target, property) { 16 | 17 | if (Reflect.has(target, property)) { 18 | 19 | return target[property]; 20 | 21 | } else { 22 | 23 | throw new Error(`Undefined property cannot be get: ${property}`); 24 | 25 | } 26 | 27 | }, 28 | 29 | set(target, property, value) { 30 | 31 | if (Reflect.has(target, property)) { 32 | 33 | target[property] = value; 34 | 35 | } else { 36 | 37 | throw new Error(`Undefined property cannot be set: ${property}`); 38 | 39 | } 40 | 41 | } 42 | 43 | }); 44 | 45 | } 46 | 47 | } 48 | 49 | export function mixin(Base, ... mixins) { 50 | 51 | if (!Base) 52 | Base = class { }; 53 | 54 | let mixed = class extends Base { 55 | 56 | constructor(... parameters) { 57 | 58 | super(... parameters); 59 | 60 | // eslint-disable-next-line no-shadow 61 | mixins.forEach(mixin => { 62 | Reflect.apply(mixin, this); 63 | }); 64 | 65 | } 66 | 67 | }; 68 | 69 | for (let mixin of mixins) // eslint-disable-line no-shadow 70 | for (let method of Object.keys(mixin.prototype)) 71 | mixed.prototype[method] = mixin.prototype[method]; 72 | 73 | return mixed; 74 | 75 | } 76 | 77 | export function serializeArrayBuffer(arrayBuffer) { 78 | 79 | let serialization = ``; 80 | 81 | for (let array = new Uint8Array(arrayBuffer), t = 0, T = array.length; t < T; ++t) 82 | serialization += String.fromCharCode(array[t]); 83 | 84 | return serialization; 85 | 86 | } 87 | 88 | export function serialize(data) { 89 | 90 | // eslint-disable-next-line no-shadow 91 | let getFormat = data => Object.keys(data).reduce((format, key) => { 92 | 93 | let value = data[key]; 94 | 95 | if (value instanceof ArrayBuffer) { 96 | format[key] = `arraybuffer`; 97 | } else if (value && value.constructor === Object) { 98 | format[key] = getFormat(value); 99 | } else { 100 | format[key] = null; 101 | } 102 | 103 | return format; 104 | 105 | }, { }); 106 | 107 | // eslint-disable-next-line no-shadow 108 | let simplify = data => Object.keys(data).reduce((simplified, key) => { 109 | 110 | let value = data[key]; 111 | 112 | if (value instanceof ArrayBuffer) { 113 | simplified[key] = serializeArrayBuffer(value); 114 | } else if (value && value.constructor === Object) { 115 | simplified[key] = simplify(value); 116 | } else { 117 | simplified[key] = value; 118 | } 119 | 120 | return simplified; 121 | 122 | }, { }); 123 | 124 | return JSON.stringify({ 125 | 126 | format: getFormat(data), 127 | tree: simplify(data) 128 | 129 | }); 130 | 131 | } 132 | 133 | export function unserializeArrayBuffer(serialization) { 134 | 135 | let buffer = new ArrayBuffer(serialization.length); 136 | let bufferView = new Uint8Array(buffer); 137 | 138 | for (let t = 0, T = bufferView.length; t < T; ++t) 139 | bufferView[t] = serialization.charCodeAt(t); 140 | 141 | return bufferView.buffer; 142 | 143 | } 144 | 145 | export function unserialize(serialization) { 146 | 147 | let complexify = (format, tree) => Object.keys(format).reduce((complexified, key) => { 148 | 149 | let type = format[key]; 150 | let node = tree[key]; 151 | 152 | if (type === `arraybuffer`) { 153 | complexified[key] = unserializeArrayBuffer(node); 154 | } else if (type && type.constructor === Object) { 155 | complexified[key] = complexify(type, node); 156 | } else { 157 | complexified[key] = node; 158 | } 159 | 160 | return complexified; 161 | 162 | }, { }); 163 | 164 | let { format, tree } = typeof serialization === `object` ? serialization : JSON.parse(serialization); 165 | return complexify(format, tree); 166 | 167 | } 168 | 169 | export function clone(input) { 170 | 171 | if (typeof input !== `object`) 172 | return input; 173 | 174 | if (input instanceof Array) 175 | return input.map(value => clone(value)); 176 | 177 | if (input instanceof ArrayBuffer) 178 | return input.slice(0); 179 | 180 | let output = {}; 181 | 182 | for (let key of Object.keys(input)) 183 | output[key] = clone(input[key]); 184 | 185 | return output; 186 | 187 | } 188 | -------------------------------------------------------------------------------- /sources/utils/TypeUtils.js: -------------------------------------------------------------------------------- 1 | export let toSigned8 = (() => { 2 | 3 | let tmp = new Int8Array(1); 4 | 5 | return n => { 6 | 7 | tmp[0] = n; 8 | 9 | return tmp[0]; 10 | 11 | }; 12 | 13 | })(); 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | resolveLoader: { 4 | root: __dirname + '/node_modules/' 5 | }, 6 | 7 | resolve: { 8 | extensions: [ '', '.js' ], 9 | root: __dirname, 10 | alias: { 11 | 'virtjs': __dirname + '/sources/' 12 | } 13 | }, 14 | 15 | entry: { 16 | 'main': [ 17 | 'virtjs/index' 18 | ] 19 | }, 20 | 21 | output: { 22 | path: __dirname + '/build/', 23 | library: 'Virtjs', 24 | libraryTarget: 'umd', 25 | filename: '[name].js', 26 | chunkFilename: '[id].js' 27 | }, 28 | 29 | module: { 30 | loaders: [ { 31 | test: /\.js$/, 32 | loader: 'babel-loader', 33 | include: __dirname + '/sources' 34 | } ] 35 | }, 36 | 37 | externals: { 38 | 'fs': true 39 | } 40 | 41 | }; 42 | --------------------------------------------------------------------------------