├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── binding.gyp ├── examples ├── .gitignore ├── 00-hello-world │ ├── index.js │ └── package.json ├── 01-fps-counter │ ├── index.js │ └── package.json ├── 02-raw-drawing │ ├── index.js │ └── package.json ├── 03-scaling │ ├── index.js │ └── package.json ├── 04-aspect-ratio │ ├── assets │ │ └── megaman.png │ ├── index.js │ └── package.json ├── 05-canvas-drawing │ ├── index.js │ └── package.json ├── 06-indirect-webgl-drawing │ ├── index.js │ └── package.json ├── 07-webgl-drawing │ ├── index.js │ └── package.json ├── 08-webgl-regl │ ├── index.js │ └── package.json ├── 09-ffmpeg-video │ ├── assets │ │ ├── image.png │ │ └── video.mp4 │ ├── ffmpeg.js │ ├── index.js │ └── package.json ├── 10-joystick │ ├── index.js │ └── package.json ├── 11-controller │ ├── index.js │ └── package.json ├── 12-sine-wave │ ├── index.js │ └── package.json ├── 13-mic-waveform │ ├── index.js │ └── package.json ├── 14-echo │ ├── index.js │ └── package.json ├── 15-ffmpeg-audio │ ├── assets │ │ └── audio.wav │ ├── ffmpeg.js │ ├── index.js │ └── package.json ├── 16-audio-thread │ ├── audio-worker.js │ ├── index.js │ └── package.json ├── 17-audio-driver │ ├── assets │ │ └── audio.wav │ ├── audio-proc.js │ ├── ffmpeg.js │ ├── index.js │ └── package.json ├── 18-clipboard-mutator │ ├── index.js │ └── package.json ├── 19-packaging │ ├── .github │ │ └── workflows │ │ │ └── build.yml │ ├── .gitignore │ ├── assets │ │ ├── image.png │ │ └── video.mp4 │ ├── package.json │ └── src │ │ ├── ffmpeg.js │ │ └── index.js ├── 20-pkg │ ├── .github │ │ └── workflows │ │ │ └── build.yml │ ├── assets │ │ └── image.bmp │ ├── package.json │ ├── scripts │ │ └── package.mjs │ └── src │ │ └── index.js ├── 21-pkg-webpack │ ├── .github │ │ └── workflows │ │ │ └── build.yml │ ├── assets │ │ └── image.bmp │ ├── package.json │ ├── scripts │ │ └── package.mjs │ ├── src │ │ ├── bmp.js │ │ └── index.js │ └── webpack.config.js └── README.md ├── package-lock.json ├── package.json ├── scripts ├── build-sdl.mjs ├── build.mjs ├── clean.mjs ├── download-release.mjs ├── download-sdl.mjs ├── install-deps-mac.sh ├── install-deps-raspbian.sh ├── install-deps-ubuntu.sh ├── install.mjs ├── release-rpi-qemu.sh ├── release.mjs ├── upload-release.mjs └── util │ ├── common.js │ └── fetch.js ├── src ├── javascript │ ├── audio │ │ ├── audio-instance.js │ │ ├── audio-playback-instance.js │ │ ├── audio-recording-instance.js │ │ ├── device.js │ │ ├── format-helpers.js │ │ ├── index.js │ │ └── prevent-exit.js │ ├── bindings.js │ ├── cleanup.js │ ├── clipboard │ │ └── index.js │ ├── controller │ │ ├── controller-instance.js │ │ ├── device.js │ │ └── index.js │ ├── enums.js │ ├── events │ │ ├── events-via-poll.js │ │ ├── index.js │ │ ├── reconcile-audio-devices.js │ │ ├── reconcile-displays.js │ │ ├── reconcile-joystick-and-controller-devices.js │ │ └── reconcile.js │ ├── globals.js │ ├── helpers.js │ ├── index.js │ ├── joystick │ │ ├── device.js │ │ ├── index.js │ │ └── joystick-instance.js │ ├── keyboard │ │ ├── index.js │ │ └── key-mapping.js │ ├── mouse │ │ └── index.js │ ├── power │ │ └── index.js │ ├── sensor │ │ ├── index.js │ │ └── sensor-instance.js │ └── video │ │ ├── display.js │ │ ├── index.js │ │ └── window.js ├── native │ ├── audio.cpp │ ├── audio.h │ ├── clipboard.cpp │ ├── clipboard.h │ ├── cocoa-global.h │ ├── cocoa-global.mm │ ├── cocoa-window.h │ ├── cocoa-window.mm │ ├── controller.cpp │ ├── controller.h │ ├── enums.cpp │ ├── enums.h │ ├── events.cpp │ ├── events.h │ ├── global.cpp │ ├── global.h │ ├── joystick.cpp │ ├── joystick.h │ ├── keyboard.cpp │ ├── keyboard.h │ ├── module.cpp │ ├── mouse.cpp │ ├── mouse.h │ ├── power.cpp │ ├── power.h │ ├── sensor.cpp │ ├── sensor.h │ ├── video.cpp │ ├── video.h │ ├── window.cpp │ └── window.h └── types │ ├── helpers.d.ts │ └── index.d.ts └── tests ├── audio.test.js ├── clipboard.test.js ├── controller.test.mjs ├── info.test.js ├── joystick.test.mjs ├── keyboard.test.js ├── mouse.test.js ├── power.test.js ├── sensor.test.js ├── video.test.js └── window.test.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | name: ${{ matrix.platform.name }} 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: 16 | - { name: 'Linux (x64)' ,os: ubuntu-22.04 } 17 | - { name: 'Linux (arm64)' ,os: ubuntu-22.04-arm } 18 | - { name: 'Windows (x64)' ,os: windows-2022 } 19 | - { name: 'Mac (x64)' ,os: macos-14 ,arch: x64 } 20 | - { name: 'Mac (arm64)' ,os: macos-14 ,arch: arm64 } 21 | 22 | runs-on: ${{ matrix.platform.os }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - if: ${{ startsWith(matrix.platform.os, 'macos-') }} 28 | run: brew update && ./scripts/install-deps-mac.sh 29 | 30 | - if: ${{ startsWith(matrix.platform.os, 'ubuntu-') }} 31 | run: sudo apt-get update && sudo ./scripts/install-deps-ubuntu.sh 32 | 33 | - env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | CROSS_COMPILE_ARCH: ${{ matrix.platform.arch }} 36 | run: npm run release 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sdl 3 | build 4 | dist 5 | publish 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /tests/ 3 | /examples/ 4 | /sdl/ 5 | /build/ 6 | /dist/ 7 | /publish/ 8 | /scripts/clean.mjs 9 | /scripts/install-deps-mac.sh 10 | /scripts/install-deps-raspbian.sh 11 | /scripts/install-deps-ubuntu.sh 12 | /scripts/release-rpi-qemu.sh 13 | /scripts/release.mjs 14 | /scripts/upload-release.mjs 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Konstantin M 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [{ 3 | 'target_name': 'sdl', 4 | 'sources': [ 5 | 'src/native/module.cpp', 6 | 'src/native/enums.cpp', 7 | 'src/native/global.cpp', 8 | 'src/native/events.cpp', 9 | 'src/native/video.cpp', 10 | 'src/native/window.cpp', 11 | 'src/native/keyboard.cpp', 12 | 'src/native/mouse.cpp', 13 | 'src/native/joystick.cpp', 14 | 'src/native/controller.cpp', 15 | 'src/native/sensor.cpp', 16 | 'src/native/audio.cpp', 17 | 'src/native/clipboard.cpp', 18 | 'src/native/power.cpp', 19 | ], 20 | 'dependencies': [ 21 | "= 1) { 25 | const fps = Math.round(frames / elapsed) 26 | 27 | window.setTitle(`FPS: ${fps}`) 28 | 29 | tic = toc 30 | frames = 0 31 | } 32 | } 33 | 34 | await setTimeout(0) 35 | } 36 | -------------------------------------------------------------------------------- /examples/01-fps-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-fps-counter", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*" 8 | }, 9 | "trustedDependencies": [ 10 | "@kmamal/sdl" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/02-raw-drawing/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | 3 | const window = sdl.video.createWindow({ resizable: true }) 4 | 5 | const render = () => { 6 | const { pixelWidth: width, pixelHeight: height } = window 7 | const stride = width * 4 8 | const buffer = Buffer.alloc(stride * height) 9 | 10 | let offset = 0 11 | for (let i = 0; i < height; i++) { 12 | for (let j = 0; j < width; j++) { 13 | buffer[offset++] = Math.floor(256 * i / height) // R 14 | buffer[offset++] = Math.floor(256 * j / width) // G 15 | buffer[offset++] = 0 // B 16 | buffer[offset++] = 255 // A 17 | } 18 | } 19 | 20 | window.render(width, height, stride, 'rgba32', buffer) 21 | } 22 | 23 | window 24 | .on('resize', render) 25 | .on('expose', render) 26 | -------------------------------------------------------------------------------- /examples/02-raw-drawing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-raw-drawing", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*" 8 | }, 9 | "trustedDependencies": [ 10 | "@kmamal/sdl" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/03-scaling/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | 3 | const window = sdl.video.createWindow({ resizable: true }) 4 | const width = 2 5 | const height = 2 6 | const stride = width * 3 7 | const buffer = Buffer.alloc(stride * height) 8 | 9 | let offset = 0 10 | // Top left pixel red 11 | buffer[offset++] = 255 // R 12 | buffer[offset++] = 0 // G 13 | buffer[offset++] = 0 // B 14 | // Top right pixel blue 15 | buffer[offset++] = 0 // R 16 | buffer[offset++] = 255 // G 17 | buffer[offset++] = 0 // B 18 | // Bottom left pixel green 19 | buffer[offset++] = 0 // R 20 | buffer[offset++] = 0 // G 21 | buffer[offset++] = 255 // B 22 | // Bottom right pixel black 23 | buffer[offset++] = 0 // R 24 | buffer[offset++] = 0 // G 25 | buffer[offset++] = 0 // B 26 | 27 | const render = () => { 28 | window.render(width, height, stride, 'rgb24', buffer, 'linear') 29 | } 30 | 31 | window 32 | .on('resize', render) 33 | .on('expose', render) 34 | -------------------------------------------------------------------------------- /examples/03-scaling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-scaling", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*" 8 | }, 9 | "trustedDependencies": [ 10 | "@kmamal/sdl" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/04-aspect-ratio/assets/megaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/04-aspect-ratio/assets/megaman.png -------------------------------------------------------------------------------- /examples/04-aspect-ratio/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import { PNG } from 'pngjs' 3 | import fs from 'node:fs' 4 | 5 | const imageData = fs.readFileSync('./assets/megaman.png') 6 | const image = PNG.sync.read(imageData) 7 | 8 | const window = sdl.video.createWindow({ resizable: true }) 9 | 10 | const viewport = { 11 | x: 0, 12 | y: 0, 13 | width: 0, 14 | height: 0, 15 | } 16 | 17 | const resize = () => { 18 | const factorX = Math.floor(window.width / image.width) 19 | const factorY = Math.floor(window.height / image.height) 20 | const factor = Math.min(factorX, factorY) 21 | 22 | viewport.width = factor * image.width 23 | viewport.height = factor * image.height 24 | 25 | viewport.x = Math.floor((window.width - viewport.width) / 2) 26 | viewport.y = Math.floor((window.height - viewport.height) / 2) 27 | 28 | render() 29 | } 30 | 31 | const render = () => { 32 | window.render( 33 | image.width, 34 | image.height, 35 | image.width * 4, 36 | 'rgba32', 37 | image.data, 38 | { dstRect: viewport }, 39 | ) 40 | } 41 | 42 | window.on('resize', resize) 43 | window.on('expose', render) 44 | -------------------------------------------------------------------------------- /examples/04-aspect-ratio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-aspect-ratio", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "pngjs": "^7.0.0" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/05-canvas-drawing/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import Canvas from '@napi-rs/canvas' 3 | 4 | const window = sdl.video.createWindow({ resizable: true }) 5 | 6 | let canvas 7 | let ctx 8 | 9 | const render = () => { 10 | const { pixelWidth: width, pixelHeight: height } = window 11 | 12 | ctx.font = `${Math.floor(height / 5)}px "Times New Roman"` 13 | ctx.fillStyle = 'red' 14 | ctx.textAlign = 'center' 15 | ctx.fillText("Hello, World!", width / 2, height / 2) 16 | 17 | const buffer = Buffer.from(ctx.getImageData(0, 0, width, height).data) 18 | window.render(width, height, width * 4, 'rgba32', buffer) 19 | } 20 | 21 | window.on('expose', render) 22 | 23 | window.on('resize', (event) => { 24 | canvas = Canvas.createCanvas(event.pixelWidth, event.pixelHeight) 25 | ctx = canvas.getContext('2d') 26 | render() 27 | }) 28 | -------------------------------------------------------------------------------- /examples/05-canvas-drawing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-canvas-drawing", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "@napi-rs/canvas": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "@napi-rs/canvas" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/06-indirect-webgl-drawing/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import createContext from 'gl' 3 | 4 | const window = sdl.video.createWindow({ 5 | resizable: true, 6 | accelerated: false, 7 | }) 8 | 9 | const { pixelWidth: width, pixelHeight: height } = window 10 | const gl = createContext(width, height) 11 | const ext = gl.getExtension('STACKGL_resize_drawingbuffer') 12 | 13 | const vertexShader = gl.createShader(gl.VERTEX_SHADER) 14 | gl.shaderSource(vertexShader, ` 15 | attribute vec2 position; 16 | varying vec2 uv; 17 | 18 | void main() { 19 | gl_Position = vec4(position, 0.0, 1.0); 20 | uv = (position + vec2(1.0)) / 2.0; 21 | } 22 | `) 23 | gl.compileShader(vertexShader) 24 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 25 | console.log("vertex shader error:", gl.getShaderInfoLog(vertexShader)) 26 | process.exit(1) 27 | } 28 | 29 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) 30 | gl.shaderSource(fragmentShader, ` 31 | precision highp float; 32 | varying vec2 uv; 33 | 34 | void main() { 35 | gl_FragColor = vec4(uv.y, uv.x, 0.5, 1.0); 36 | } 37 | `) 38 | gl.compileShader(fragmentShader) 39 | if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 40 | console.log("fragment shader error:", gl.getShaderInfoLog(fragmentShader)) 41 | process.exit(1) 42 | } 43 | 44 | const program = gl.createProgram() 45 | gl.attachShader(program, vertexShader) 46 | gl.attachShader(program, fragmentShader) 47 | gl.linkProgram(program) 48 | gl.validateProgram(program) 49 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 50 | console.log("linking error:", gl.getProgramInfoLog(program)) 51 | process.exit() 52 | } 53 | gl.detachShader(program, vertexShader) 54 | gl.detachShader(program, fragmentShader) 55 | gl.deleteShader(vertexShader) 56 | gl.deleteShader(fragmentShader) 57 | 58 | const arrayBuffer = gl.createBuffer() 59 | gl.bindBuffer(gl.ARRAY_BUFFER, arrayBuffer) 60 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 61 | ...[ -1, 1 ], 62 | ...[ 1, 1 ], 63 | ...[ -1, -1 ], 64 | ...[ 1, -1 ], 65 | ]), gl.STATIC_DRAW) 66 | const attribPosition = gl.getAttribLocation(program, 'position') 67 | gl.vertexAttribPointer(attribPosition, 2, gl.FLOAT, false, 0, 0) 68 | gl.enableVertexAttribArray(attribPosition) 69 | 70 | gl.useProgram(program) 71 | 72 | gl.clearColor(0, 0, 0, 1) 73 | 74 | const render = () => { 75 | gl.clear(gl.COLOR_BUFFER_BIT) 76 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) 77 | 78 | const { pixelWidth: w, pixelHeight: h } = window 79 | const buffer = new Uint8Array(w * h * 4) 80 | gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, buffer) 81 | window.render(w, h, w * 4, 'rgba32', Buffer.from(buffer)) 82 | } 83 | 84 | window.on('expose', render) 85 | 86 | window.on('resize', ({ pixelWidth: w, pixelHeight: h }) => { 87 | ext.resize(w, h) 88 | gl.viewport(0, 0, w, h) 89 | 90 | render() 91 | }) 92 | -------------------------------------------------------------------------------- /examples/06-indirect-webgl-drawing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-indirect-webgl-drawing", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "gl": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "gl" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/07-webgl-drawing/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import createContext from '@kmamal/gl' 3 | 4 | const window = sdl.video.createWindow({ 5 | resizable: true, 6 | opengl: true, 7 | }) 8 | 9 | const { pixelWidth: width, pixelHeight: height, native } = window 10 | const gl = createContext(width, height, { window: native }) 11 | const ext = gl.getExtension('STACKGL_resize_drawingbuffer') 12 | 13 | const vertexShader = gl.createShader(gl.VERTEX_SHADER) 14 | gl.shaderSource(vertexShader, ` 15 | attribute vec2 position; 16 | varying vec2 uv; 17 | 18 | void main() { 19 | gl_Position = vec4(position, 0.0, 1.0); 20 | uv = (position + vec2(1.0)) / 2.0; 21 | } 22 | `) 23 | gl.compileShader(vertexShader) 24 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 25 | console.log("vertex shader error:", gl.getShaderInfoLog(vertexShader)) 26 | process.exit(1) 27 | } 28 | 29 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) 30 | gl.shaderSource(fragmentShader, ` 31 | precision highp float; 32 | varying vec2 uv; 33 | 34 | void main() { 35 | gl_FragColor = vec4(uv.y, uv.x, 0.5, 1.0); 36 | } 37 | `) 38 | gl.compileShader(fragmentShader) 39 | if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 40 | console.log("fragment shader error:", gl.getShaderInfoLog(fragmentShader)) 41 | process.exit(1) 42 | } 43 | 44 | const program = gl.createProgram() 45 | gl.attachShader(program, vertexShader) 46 | gl.attachShader(program, fragmentShader) 47 | gl.linkProgram(program) 48 | gl.validateProgram(program) 49 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 50 | console.log("linking error:", gl.getProgramInfoLog(program)) 51 | process.exit() 52 | } 53 | gl.detachShader(program, vertexShader) 54 | gl.detachShader(program, fragmentShader) 55 | gl.deleteShader(vertexShader) 56 | gl.deleteShader(fragmentShader) 57 | 58 | const arrayBuffer = gl.createBuffer() 59 | gl.bindBuffer(gl.ARRAY_BUFFER, arrayBuffer) 60 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 61 | ...[ -1, 1 ], 62 | ...[ 1, 1 ], 63 | ...[ -1, -1 ], 64 | ...[ 1, -1 ], 65 | ]), gl.STATIC_DRAW) 66 | const attribPosition = gl.getAttribLocation(program, 'position') 67 | gl.vertexAttribPointer(attribPosition, 2, gl.FLOAT, false, 0, 0) 68 | gl.enableVertexAttribArray(attribPosition) 69 | 70 | gl.useProgram(program) 71 | 72 | gl.clearColor(1, 0, 0, 1) 73 | 74 | const render = () => { 75 | gl.clear(gl.COLOR_BUFFER_BIT) 76 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) 77 | gl.swap() 78 | } 79 | 80 | window.on('expose', render) 81 | 82 | window.on('resize', ({ width: w, height: h, pixelWidth: pw, pixelHeight: ph }) => { 83 | ext.resize(pw, ph) 84 | gl.viewport(0, 0, w, h) 85 | gl.swap() 86 | 87 | render() 88 | }) 89 | -------------------------------------------------------------------------------- /examples/07-webgl-drawing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-webgl-drawing", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/gl": "*", 8 | "@kmamal/sdl": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/gl", 12 | "@kmamal/sdl" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/08-webgl-regl/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import createContext from '@kmamal/gl' 3 | import createRegl from 'regl' 4 | 5 | const window = sdl.video.createWindow({ 6 | resizable: true, 7 | opengl: true, 8 | }) 9 | 10 | const { pixelWidth: width, pixelHeight: height, native } = window 11 | const gl = createContext(width, height, { window: native }) 12 | const ext = gl.getExtension('STACKGL_resize_drawingbuffer') 13 | const regl = createRegl({ gl }) 14 | 15 | const drawTriangle = regl({ 16 | frag: ` 17 | precision mediump float; 18 | uniform vec4 color; 19 | void main() { 20 | gl_FragColor = color; 21 | }`, 22 | 23 | vert: ` 24 | precision mediump float; 25 | attribute vec2 position; 26 | void main() { 27 | gl_Position = vec4(position, 0, 1); 28 | }`, 29 | 30 | attributes: { 31 | position: regl.buffer([ 32 | [ 0, 0.5 ], 33 | [ 0.5, -0.5 ], 34 | [ -0.5, -0.5 ], 35 | ]), 36 | }, 37 | 38 | uniforms: { 39 | color: regl.prop('color'), 40 | }, 41 | 42 | count: 3, 43 | }) 44 | 45 | 46 | let tic = Date.now() 47 | let toc 48 | let frames = 0 49 | 50 | const render = () => { 51 | if (window.destroyed) { 52 | clearInterval(interval) 53 | return 54 | } 55 | 56 | // Render 57 | { 58 | regl.clear({ 59 | color: [ 0, 0, 0, 1 ], 60 | depth: 1, 61 | }) 62 | 63 | const a = (Date.now() / 1000) % (2 * Math.PI) 64 | 65 | drawTriangle({ color: [ 66 | Math.cos(a), 67 | Math.sin(a * 0.8), 68 | Math.cos(a * 3), 69 | 1, 70 | ] }) 71 | 72 | gl.swap() 73 | } 74 | 75 | // Count frames 76 | { 77 | frames++ 78 | toc = Date.now() 79 | const elapsed = toc - tic 80 | if (elapsed >= 1e3) { 81 | const fps = Math.round(frames / (elapsed / 1e3)) 82 | 83 | window.setTitle(`FPS: ${fps}`) 84 | 85 | tic = toc 86 | frames = 0 87 | } 88 | } 89 | } 90 | 91 | window.on('resize', ({ width: w, height: h, pixelWidth: pw, pixelHeight: ph }) => { 92 | ext.resize(pw, ph) 93 | gl.viewport(0, 0, w, h) 94 | gl.swap() 95 | 96 | render() 97 | }) 98 | 99 | const interval = setInterval(render, 1e3 / 30) 100 | -------------------------------------------------------------------------------- /examples/08-webgl-regl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-webgl-regl", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/gl": "*", 8 | "@kmamal/sdl": "*", 9 | "regl": "*" 10 | }, 11 | "trustedDependencies": [ 12 | "@kmamal/gl", 13 | "@kmamal/sdl" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/09-ffmpeg-video/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/09-ffmpeg-video/assets/image.png -------------------------------------------------------------------------------- /examples/09-ffmpeg-video/assets/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/09-ffmpeg-video/assets/video.mp4 -------------------------------------------------------------------------------- /examples/09-ffmpeg-video/ffmpeg.js: -------------------------------------------------------------------------------- 1 | import ffmpeg from 'ffmpeg-static' 2 | import { spawn } from 'node:child_process' 3 | import path from 'node:path' 4 | 5 | export const loadImage = async (filepath, options) => (await loadVideo(filepath, options))[0] 6 | 7 | export const loadVideo = async (filepath, { width, height, framerate }) => { 8 | const proc = spawn( 9 | ffmpeg, 10 | [ 11 | [ '-i', path.relative(process.cwd(), filepath).replaceAll(path.sep, path.posix.sep) ], 12 | '-filter:v', 13 | [ 14 | framerate && `fps=fps=${framerate}`, 15 | `scale=${width}:${height}`, 16 | 'format=pix_fmts=rgb24', 17 | ].filter(Boolean).join(','), 18 | [ '-f', 'rawvideo' ], 19 | '-', 20 | ].flat(), 21 | ) 22 | 23 | const frameSize = width * height * 3 24 | const frames = [] 25 | let chunks = [] 26 | let size = 0 27 | 28 | const append = (data) => { 29 | const end = frameSize - size 30 | const chunk = data.slice(0, end) 31 | chunks.push(chunk) 32 | size += chunk.length 33 | 34 | if (size === frameSize) { 35 | frames.push(Buffer.concat(chunks)) 36 | chunks = [] 37 | size = 0 38 | append(data.slice(end)) 39 | } 40 | } 41 | 42 | proc.stdout.on('data', append) 43 | 44 | const stderrChunks = [] 45 | proc.stderr.on('data', (chunk) => { 46 | stderrChunks.push(chunk.toString()) 47 | }) 48 | 49 | return await new Promise((resolve, reject) => { 50 | proc.on('close', (code) => { 51 | code 52 | ? reject(Object.assign(new Error(`exit code ${code}`), { 53 | stderr: stderrChunks.join(''), 54 | })) 55 | : resolve(frames) 56 | }) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /examples/09-ffmpeg-video/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import path from 'node:path' 3 | import url from 'node:url' 4 | import { setTimeout } from 'node:timers/promises' 5 | import { loadImage, loadVideo } from './ffmpeg.js' 6 | 7 | const framerate = 25 8 | 9 | const window = sdl.video.createWindow() 10 | const { pixelWidth: width, pixelHeight: height } = window 11 | 12 | const dir = path.dirname(url.fileURLToPath(import.meta.url)) 13 | const [ image, video ] = await Promise.all([ 14 | loadImage(path.join(dir, 'assets/image.png'), { width, height }), 15 | loadVideo(path.join(dir, 'assets/video.mp4'), { width, height, framerate }), 16 | ]) 17 | 18 | let startTime = null 19 | let lastIndex = null 20 | 21 | window.on('mouseButtonUp', () => { 22 | startTime = startTime ? null : Date.now() 23 | render() 24 | }) 25 | 26 | const render = async () => { 27 | while (!window.destroyed) { 28 | // Render pause image 29 | if (!startTime) { 30 | window.render(width, height, width * 3, 'rgb24', image) 31 | return 32 | } 33 | 34 | // Render video 35 | { 36 | const time = (Date.now() - startTime) / 1e3 37 | const index = Math.floor(time * framerate) 38 | 39 | if (index === lastIndex) { continue } 40 | 41 | if (index >= video.length) { 42 | startTime = null 43 | lastIndex = null 44 | 45 | window.render(width, height, width * 3, 'rgb24', image) 46 | return 47 | } 48 | 49 | window.render(width, height, width * 3, 'rgb24', video[index]) 50 | lastIndex = index 51 | } 52 | 53 | await setTimeout(0) 54 | } 55 | } 56 | 57 | render() 58 | -------------------------------------------------------------------------------- /examples/09-ffmpeg-video/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-ffmpeg-video", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "ffmpeg-static": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "ffmpeg-static" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/10-joystick/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import Canvas from '@napi-rs/canvas' 3 | 4 | const window = sdl.video.createWindow({ resizable: true }) 5 | let canvas 6 | let ctx 7 | 8 | Canvas.GlobalFonts.loadSystemFonts() 9 | 10 | const instances = new Set() 11 | 12 | const doRender = () => { 13 | if (window.destroyed) { return } 14 | 15 | const { 16 | pixelWidth: W, 17 | pixelHeight: H, 18 | } = window 19 | 20 | let x = 0 21 | let y = 0 22 | let maxX = 0 23 | let maxY = 0 24 | 25 | ctx.font = '12px "DejaVu Sans Mono"' 26 | ctx.textAlign = 'left' 27 | ctx.textBaseline = 'top' 28 | 29 | ctx.fillStyle = 'black' 30 | ctx.fillRect(0, 0, W, H) 31 | 32 | ctx.fillStyle = 'white' 33 | 34 | if (instances.size === 0) { 35 | x += 20 36 | y += 20 37 | 38 | const message = "No joysticks connected" 39 | ctx.fillText(message, x, y) 40 | 41 | const metrics = ctx.measureText(message) 42 | maxX = Math.ceil(x + metrics.width) 43 | maxY = Math.ceil(y + metrics.actualBoundingBoxDescent) 44 | } 45 | else { 46 | for (const instance of instances.values()) { 47 | const { 48 | device: { 49 | id, 50 | name, 51 | }, 52 | axes, 53 | buttons, 54 | balls, 55 | hats, 56 | } = instance 57 | 58 | x += 20 59 | y += 20 60 | 61 | ctx.fillText(`[${id}] ${name}`, x, y) 62 | y += 20 63 | const topY = y 64 | 65 | { 66 | ctx.fillText("Axes", x, y) 67 | y += 20 68 | 69 | for (let i = 0; i < axes.length; i++) { 70 | ctx.fillText(`[${i}]: ${axes[i].toFixed(2)}`, x, y) 71 | y += 20 72 | } 73 | 74 | x += 120 75 | maxY = Math.max(maxY, y) 76 | y = topY 77 | } 78 | 79 | { 80 | ctx.fillText("Buttons", x, y) 81 | y += 20 82 | 83 | for (let i = 0; i < buttons.length; i++) { 84 | ctx.fillText(`[${i}]: ${buttons[i]}`, x, y) 85 | y += 20 86 | } 87 | 88 | x += 120 89 | maxY = Math.max(maxY, y) 90 | y = topY 91 | } 92 | 93 | { 94 | ctx.fillText("Balls", x, y) 95 | y += 20 96 | 97 | for (let i = 0; i < balls.length; i++) { 98 | const ball = balls[i] 99 | ctx.fillText(`[${i}]: ${ball.x.toFixed(2)}, ${ball.y.toFixed(2)}`, x, y) 100 | y += 20 101 | } 102 | 103 | x += 120 104 | maxY = Math.max(maxY, y) 105 | y = topY 106 | } 107 | 108 | { 109 | ctx.fillText("Hats", x, y) 110 | y += 20 111 | 112 | for (let i = 0; i < hats.length; i++) { 113 | ctx.fillText(`[${i}]: ${hats[i]}`, x, y) 114 | y += 20 115 | } 116 | 117 | x += 120 118 | maxY = Math.max(maxY, y) 119 | y = topY 120 | } 121 | 122 | y = maxY 123 | maxX = Math.max(maxX, x) 124 | x = 0 125 | } 126 | } 127 | 128 | maxY += 20 129 | maxX += 20 130 | 131 | if (maxX !== W || maxY !== H) { 132 | window.setSizeInPixels(maxX, maxY) 133 | } 134 | else { 135 | const buffer = Buffer.from(ctx.getImageData(0, 0, W, H).data) 136 | window.render(W, H, W * 4, 'rgba32', buffer) 137 | } 138 | } 139 | 140 | let nextRender = null 141 | const render = () => { 142 | if (nextRender) { return } 143 | nextRender = setTimeout(() => { 144 | nextRender = null 145 | doRender() 146 | }) 147 | } 148 | 149 | window.on('expose', render) 150 | window.on('resize', (event) => { 151 | canvas = Canvas.createCanvas(event.pixelWidth, event.pixelHeight) 152 | ctx = canvas.getContext('2d') 153 | }) 154 | 155 | const openJoystick = (device) => { 156 | const instance = sdl.joystick.openDevice(device) 157 | instances.add(instance) 158 | 159 | instance.on('*', (eventType) => { 160 | if (eventType === 'close') { 161 | instances.delete(instance) 162 | } 163 | render() 164 | }) 165 | } 166 | 167 | sdl.joystick.on('deviceAdd', (event) => { 168 | openJoystick(event.device) 169 | render() 170 | }) 171 | 172 | sdl.joystick.on('deviceRemove', render) 173 | 174 | for (const device of sdl.joystick.devices) { 175 | openJoystick(device) 176 | } 177 | 178 | window.on('close', () => { 179 | for (const instance of instances.values()) { 180 | instance.close() 181 | } 182 | 183 | sdl.joystick.removeAllListeners('deviceAdd') 184 | sdl.joystick.removeAllListeners('deviceRemove') 185 | }) 186 | -------------------------------------------------------------------------------- /examples/10-joystick/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-joystick", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "@napi-rs/canvas": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "@napi-rs/canvas" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/11-controller/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import Canvas from '@napi-rs/canvas' 3 | 4 | const window = sdl.video.createWindow({ resizable: true }) 5 | let canvas 6 | let ctx 7 | 8 | Canvas.GlobalFonts.loadSystemFonts() 9 | 10 | const instances = new Set() 11 | 12 | const doRender = () => { 13 | if (window.destroyed) { return } 14 | 15 | const { 16 | pixelWidth: W, 17 | pixelHeight: H, 18 | } = window 19 | 20 | let x = 0 21 | let y = 0 22 | let maxX = 0 23 | let maxY = 0 24 | 25 | ctx.font = '12px "DejaVu Sans Mono"' 26 | ctx.textAlign = 'left' 27 | ctx.textBaseline = 'top' 28 | 29 | ctx.fillStyle = 'black' 30 | ctx.fillRect(0, 0, W, H) 31 | 32 | ctx.fillStyle = 'white' 33 | 34 | if (instances.size === 0) { 35 | x += 20 36 | y += 20 37 | 38 | const message = "No controllers connected" 39 | ctx.fillText(message, x, y) 40 | 41 | const metrics = ctx.measureText(message) 42 | maxX = Math.ceil(x + metrics.width) 43 | maxY = Math.ceil(y + metrics.actualBoundingBoxDescent) 44 | } 45 | else { 46 | for (const instance of instances.values()) { 47 | const { 48 | device: { 49 | id, 50 | name, 51 | }, 52 | axes, 53 | buttons, 54 | } = instance 55 | 56 | x += 20 57 | y += 20 58 | 59 | ctx.fillText(`[${id}] ${name}`, x, y) 60 | y += 20 61 | const topY = y 62 | 63 | { 64 | ctx.fillText("Axes", x, y) 65 | y += 20 66 | 67 | for (const [ key, value ] of Object.entries(axes)) { 68 | ctx.fillText(`${key}: ${value.toFixed(2)}`, x, y) 69 | y += 20 70 | } 71 | 72 | x += 200 73 | maxY = Math.max(maxY, y) 74 | y = topY 75 | } 76 | 77 | { 78 | ctx.fillText("Buttons", x, y) 79 | y += 20 80 | 81 | for (const [ key, value ] of Object.entries(buttons)) { 82 | ctx.fillText(`${key}: ${value}`, x, y) 83 | y += 20 84 | } 85 | 86 | x += 200 87 | maxY = Math.max(maxY, y) 88 | y = topY 89 | } 90 | 91 | y = maxY 92 | maxX = Math.max(maxX, x) 93 | x = 0 94 | } 95 | } 96 | 97 | maxY += 20 98 | maxX += 20 99 | 100 | if (maxX !== W || maxY !== H) { 101 | window.setSizeInPixels(maxX, maxY) 102 | } 103 | else { 104 | const buffer = Buffer.from(ctx.getImageData(0, 0, W, H).data) 105 | window.render(W, H, W * 4, 'rgba32', buffer) 106 | } 107 | } 108 | 109 | let nextRender = null 110 | const render = () => { 111 | if (nextRender) { return } 112 | nextRender = setTimeout(() => { 113 | nextRender = null 114 | doRender() 115 | }) 116 | } 117 | 118 | window.on('expose', render) 119 | window.on('resize', (event) => { 120 | canvas = Canvas.createCanvas(event.pixelWidth, event.pixelHeight) 121 | ctx = canvas.getContext('2d') 122 | }) 123 | 124 | const openController = (device) => { 125 | const instance = sdl.controller.openDevice(device) 126 | instances.add(instance) 127 | 128 | instance.on('*', (eventType) => { 129 | if (eventType === 'close') { 130 | instances.delete(instance) 131 | } 132 | render() 133 | }) 134 | } 135 | 136 | sdl.controller.on('deviceAdd', (event) => { 137 | openController(event.device) 138 | render() 139 | }) 140 | 141 | sdl.controller.on('deviceRemove', render) 142 | 143 | for (const device of sdl.controller.devices) { 144 | openController(device) 145 | } 146 | 147 | window.on('close', () => { 148 | for (const instance of instances.values()) { 149 | instance.close() 150 | } 151 | 152 | sdl.controller.removeAllListeners('deviceAdd') 153 | sdl.controller.removeAllListeners('deviceRemove') 154 | }) 155 | -------------------------------------------------------------------------------- /examples/11-controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-controller", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "@napi-rs/canvas": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "@napi-rs/canvas" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/12-sine-wave/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | 3 | const TWO_PI = 2 * Math.PI 4 | 5 | const playbackInstance = sdl.audio.openDevice({ type: 'playback' }) 6 | const { 7 | channels, 8 | frequency, 9 | bytesPerSample, 10 | minSampleValue, 11 | maxSampleValue, 12 | zeroSampleValue, 13 | } = playbackInstance 14 | const range = maxSampleValue - minSampleValue 15 | const amplitude = range / 2 16 | 17 | const sineAmplitude = 0.3 * amplitude 18 | const sineNote = 440 19 | const sinePeriod = 1 / sineNote 20 | 21 | const duration = 3 22 | const numFrames = duration * frequency 23 | const numSamples = numFrames * channels 24 | const numBytes = numSamples * bytesPerSample 25 | const buffer = Buffer.alloc(numBytes) 26 | 27 | let offset = 0 28 | for (let i = 0; i < numFrames; i++) { 29 | const time = i / frequency 30 | const angle = time / sinePeriod * TWO_PI 31 | const sample = zeroSampleValue + Math.sin(angle) * sineAmplitude 32 | for (let j = 0; j < channels; j++) { 33 | offset = playbackInstance.writeSample(buffer, sample, offset) 34 | } 35 | } 36 | 37 | playbackInstance.enqueue(buffer) 38 | playbackInstance.play() 39 | -------------------------------------------------------------------------------- /examples/12-sine-wave/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-sine-wave", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*" 8 | }, 9 | "trustedDependencies": [ 10 | "@kmamal/sdl" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/13-mic-waveform/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import Canvas from '@napi-rs/canvas' 3 | import { setTimeout } from 'timers/promises' 4 | 5 | const window = sdl.video.createWindow() 6 | const { pixelWidth: width, pixelHeight: height } = window 7 | const canvas = Canvas.createCanvas(width, height) 8 | const ctx = canvas.getContext('2d') 9 | 10 | const channels = 1 11 | const buffered = 128 12 | const recordingInstance = sdl.audio.openDevice({ type: 'recording' }, { 13 | channels, 14 | buffered, 15 | }) 16 | const { 17 | frequency, 18 | bytesPerSample, 19 | minSampleValue, 20 | maxSampleValue, 21 | zeroSampleValue, 22 | } = recordingInstance 23 | const range = maxSampleValue - minSampleValue 24 | const amplitude = range / 2 25 | 26 | const duration = 5 27 | const numSamples = duration * frequency 28 | const numBytes = numSamples * bytesPerSample 29 | const audioBuffer = Buffer.alloc(numBytes, 0) 30 | 31 | recordingInstance.play() 32 | 33 | const supersampling = 4 34 | 35 | while (!window.destroyed) { 36 | // Read new audio samples 37 | { 38 | const { queued } = recordingInstance 39 | if (queued === 0) { 40 | await setTimeout(1) 41 | continue 42 | } 43 | audioBuffer.copy(audioBuffer, 0, queued) 44 | recordingInstance.dequeue(audioBuffer.slice(-queued)) 45 | } 46 | 47 | // Render 48 | { 49 | ctx.fillStyle = 'black' 50 | ctx.fillRect(0, 0, width, height) 51 | ctx.fillStyle = 'white' 52 | 53 | ctx.save() 54 | ctx.translate(0, height / 2) 55 | ctx.scale(1, -height / 2) 56 | ctx.lineWidth = 1 / width 57 | { 58 | let min = 0 59 | let max = 0 60 | let lastX = -1 61 | for (let i = 0; i < numSamples; i++) { 62 | const x = Math.floor((i / numSamples) * width * supersampling) 63 | 64 | if (x > lastX) { 65 | const y = (min - zeroSampleValue) / amplitude 66 | const h = (max - min) / amplitude 67 | ctx.fillRect(lastX / supersampling, y, 1, Math.max(1 / height, h)) 68 | lastX = x 69 | min = Infinity 70 | max = -Infinity 71 | } 72 | 73 | const sample = recordingInstance.readSample(audioBuffer, i * bytesPerSample) 74 | max = Math.max(max, sample) 75 | min = Math.min(min, sample) 76 | } 77 | } 78 | ctx.restore() 79 | 80 | const pixelBuffer = Buffer.from(ctx.getImageData(0, 0, width, height).data) 81 | window.render(width, height, width * 4, 'bgra32', pixelBuffer) 82 | } 83 | 84 | await setTimeout(0) 85 | } 86 | -------------------------------------------------------------------------------- /examples/13-mic-waveform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-mic-waveform", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "@napi-rs/canvas": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "@napi-rs/canvas" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/14-echo/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import { setTimeout } from 'timers/promises' 3 | 4 | const buffered = 128 5 | const options = { buffered } 6 | 7 | const recordingInstance = sdl.audio.openDevice({ type: 'recording' }, options) 8 | const playbackInstance = sdl.audio.openDevice({ type: 'playback' }, options) 9 | 10 | const { frequency, bytesPerSample } = playbackInstance 11 | 12 | const duration = 0.25 13 | const numSamples = duration * frequency 14 | const numBytes = numSamples * bytesPerSample 15 | const buffer = Buffer.alloc(numBytes, 0) 16 | 17 | recordingInstance.play() 18 | playbackInstance.play() 19 | 20 | for (;;) { 21 | const { queued } = recordingInstance 22 | 23 | if (queued === 0) { 24 | await setTimeout(1) 25 | continue 26 | } 27 | 28 | // Copy new samples 29 | const discarded = buffer.slice(0, queued) 30 | playbackInstance.enqueue(discarded) 31 | buffer.copy(buffer, 0, queued) 32 | recordingInstance.dequeue(buffer.slice(-queued)) 33 | 34 | // Apply effect 35 | const offset = buffer.length - discarded.length 36 | for (let i = 0; i < discarded.length; i += bytesPerSample) { 37 | const a = recordingInstance.readSample(buffer, offset + i) 38 | const b = recordingInstance.readSample(discarded, i) 39 | recordingInstance.writeSample(buffer, a + b * 0.5, offset + i) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/14-echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-echo", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*" 8 | }, 9 | "trustedDependencies": [ 10 | "@kmamal/sdl" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/15-ffmpeg-audio/assets/audio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/15-ffmpeg-audio/assets/audio.wav -------------------------------------------------------------------------------- /examples/15-ffmpeg-audio/ffmpeg.js: -------------------------------------------------------------------------------- 1 | import ffmpeg from 'ffmpeg-static' 2 | import { spawn } from 'node:child_process' 3 | import path from 'node:path' 4 | 5 | export const loadAudio = async (filepath, { channels, frequency }) => { 6 | const proc = spawn( 7 | ffmpeg, 8 | [ 9 | [ '-i', path.relative(process.cwd(), filepath).replaceAll(path.sep, path.posix.sep) ], 10 | channels && [ '-ac', channels ], 11 | frequency && [ '-ar', frequency ], 12 | [ '-f', 'f32le' ], 13 | [ '-c:a', 'pcm_f32le' ], 14 | '-', 15 | ].flat(), 16 | ) 17 | 18 | const chunks = [] 19 | proc.stdout.on('data', (chunk) => { 20 | chunks.push(chunk) 21 | }) 22 | 23 | const stderrChunks = [] 24 | proc.stderr.on('data', (chunk) => { 25 | stderrChunks.push(chunk.toString()) 26 | }) 27 | 28 | return await new Promise((resolve, reject) => { 29 | proc.on('close', (code) => { 30 | code 31 | ? reject(Object.assign(new Error(`exit code ${code}`), { 32 | stderr: stderrChunks.join(''), 33 | })) 34 | : resolve(Buffer.concat(chunks)) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /examples/15-ffmpeg-audio/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import path from 'node:path' 3 | import url from 'node:url' 4 | import { loadAudio } from './ffmpeg.js' 5 | 6 | const channels = 1 7 | const frequency = 48e3 8 | const audioInstance = sdl.audio.openDevice({ type: 'playback' }, { 9 | channels, 10 | frequency, 11 | format: 'f32lsb', 12 | }) 13 | 14 | const dir = path.dirname(url.fileURLToPath(import.meta.url)) 15 | const buffer = await loadAudio( 16 | path.join(dir, 'assets/audio.wav'), 17 | { channels, frequency }, 18 | ) 19 | 20 | audioInstance.enqueue(buffer) 21 | audioInstance.play() 22 | -------------------------------------------------------------------------------- /examples/15-ffmpeg-audio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-ffmpeg-audio", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "ffmpeg-static": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "ffmpeg-static" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/16-audio-thread/audio-worker.js: -------------------------------------------------------------------------------- 1 | import { workerData, parentPort } from 'node:worker_threads' 2 | import sdlHelpers from '@kmamal/sdl/helpers' 3 | 4 | const TWO_PI = 2 * Math.PI 5 | 6 | const { 7 | channels, 8 | frequency, 9 | buffered, 10 | format, 11 | } = workerData 12 | 13 | const bytesPerSample = sdlHelpers.audio.bytesPerSample(format) 14 | const minSampleValue = sdlHelpers.audio.minSampleValue(format) 15 | const maxSampleValue = sdlHelpers.audio.maxSampleValue(format) 16 | const zeroSampleValue = sdlHelpers.audio.zeroSampleValue(format) 17 | 18 | const range = maxSampleValue - minSampleValue 19 | const amplitude = range / 2 20 | 21 | const sineAmplitude = 0.3 * amplitude 22 | const sineNote = 440 23 | const sinePeriod = 1 / sineNote 24 | let index = 0 25 | 26 | const startTime = Date.now() 27 | const leadTime = (buffered / frequency) * 1e3 28 | let lastTime = startTime - leadTime 29 | 30 | setInterval(() => { 31 | const now = Date.now() 32 | const elapsed = now - lastTime 33 | if (elapsed === 0) { return } 34 | const numFrames = Math.floor(elapsed / 1e3 * frequency) 35 | if (numFrames === 0) { return } 36 | lastTime = now 37 | 38 | const numSamples = numFrames * channels 39 | const numBytes = numSamples * bytesPerSample 40 | const buffer = Buffer.alloc(numBytes) 41 | 42 | let offset = 0 43 | for (let i = 0; i < numFrames; i++) { 44 | const time = index++ / frequency 45 | const angle = time / sinePeriod * TWO_PI 46 | const sample = zeroSampleValue + Math.sin(angle) * sineAmplitude 47 | for (let j = 0; j < channels; j++) { 48 | offset = sdlHelpers.audio.writeSample(format, buffer, sample, offset) 49 | } 50 | } 51 | 52 | const arrayBuffer = buffer.buffer 53 | parentPort.postMessage(arrayBuffer, [ arrayBuffer ]) 54 | }, 0) 55 | -------------------------------------------------------------------------------- /examples/16-audio-thread/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import { Worker } from 'node:worker_threads' 3 | import { fileURLToPath } from 'node:url' 4 | import Path from 'node:path' 5 | 6 | const playbackInstance = sdl.audio.openDevice({ type: 'playback' }) 7 | const { 8 | channels, 9 | frequency, 10 | buffered, 11 | format, 12 | } = playbackInstance 13 | 14 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 15 | const workerPath = Path.join(__dirname, 'audio-worker.js') 16 | 17 | const worker = new Worker(workerPath, { 18 | workerData: { 19 | channels, 20 | frequency, 21 | buffered, 22 | format, 23 | }, 24 | }) 25 | 26 | worker 27 | .on('error', (error) => { 28 | console.error("Worker Error:", error) 29 | process.exit(1) 30 | }) 31 | .on('message', (arrayBuffer) => { 32 | playbackInstance.enqueue(Buffer.from(arrayBuffer)) 33 | }) 34 | .once('message', () => { playbackInstance.play() }) 35 | -------------------------------------------------------------------------------- /examples/16-audio-thread/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-audio-thread", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*" 8 | }, 9 | "trustedDependencies": [ 10 | "@kmamal/sdl" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/17-audio-driver/assets/audio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/17-audio-driver/assets/audio.wav -------------------------------------------------------------------------------- /examples/17-audio-driver/audio-proc.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import { loadAudio } from './ffmpeg.js' 3 | import path from 'node:path' 4 | import url from 'node:url' 5 | 6 | const channels = 1 7 | const frequency = 48e3 8 | 9 | const dir = path.dirname(url.fileURLToPath(import.meta.url)) 10 | const audioBuffer = await loadAudio( 11 | path.join(dir, 'assets/audio.wav'), 12 | { channels, frequency }, 13 | ) 14 | 15 | let audioInstance 16 | let startTime 17 | let interval 18 | 19 | const play = (time = 0) => { 20 | if (audioInstance) { return } 21 | audioInstance = sdl.audio.openDevice({ type: 'playback' }, { 22 | channels, 23 | frequency, 24 | format: 'f32lsb', 25 | }) 26 | 27 | const skippedFrames = Math.round(frequency * time / 1e3) 28 | const skippedSamples = skippedFrames * channels 29 | const skippedBytes = skippedSamples * audioInstance.bytesPerSample 30 | audioInstance.enqueue(audioBuffer.slice(skippedBytes)) 31 | audioInstance.play() 32 | 33 | startTime = Date.now() - time 34 | interval = setInterval(() => { 35 | if (audioInstance.queued === 0) { 36 | process.send({ type: 'end' }) 37 | stop() 38 | return 39 | } 40 | 41 | process.send({ type: 'time', time: Date.now() - startTime }) 42 | }, 100) 43 | } 44 | 45 | const stop = () => { 46 | if (!audioInstance) { return } 47 | audioInstance.pause() 48 | audioInstance.close() 49 | audioInstance = null 50 | clearInterval(interval) 51 | process.send({ type: 'time', time: 0 }) 52 | } 53 | 54 | process 55 | .on('message', (message) => { 56 | switch (message.type) { 57 | case 'play': { play(message.time) } break 58 | case 'stop': { stop() } break 59 | // No default 60 | } 61 | }) 62 | .send('started') 63 | -------------------------------------------------------------------------------- /examples/17-audio-driver/ffmpeg.js: -------------------------------------------------------------------------------- 1 | import ffmpeg from 'ffmpeg-static' 2 | import { spawn } from 'node:child_process' 3 | import path from 'node:path' 4 | 5 | export const loadAudio = async (filepath, { channels, frequency }) => { 6 | const proc = spawn( 7 | ffmpeg, 8 | [ 9 | [ '-i', path.relative(process.cwd(), filepath).replaceAll(path.sep, path.posix.sep) ], 10 | channels && [ '-ac', channels ], 11 | frequency && [ '-ar', frequency ], 12 | [ '-f', 'f32le' ], 13 | [ '-c:a', 'pcm_f32le' ], 14 | '-', 15 | ].flat(), 16 | ) 17 | 18 | const chunks = [] 19 | proc.stdout.on('data', (chunk) => { 20 | chunks.push(chunk) 21 | }) 22 | 23 | const stderrChunks = [] 24 | proc.stderr.on('data', (chunk) => { 25 | stderrChunks.push(chunk.toString()) 26 | }) 27 | 28 | return await new Promise((resolve, reject) => { 29 | proc.on('close', (code) => { 30 | code 31 | ? reject(Object.assign(new Error(`exit code ${code}`), { 32 | stderr: stderrChunks.join(''), 33 | })) 34 | : resolve(Buffer.concat(chunks)) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /examples/17-audio-driver/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import Canvas from '@napi-rs/canvas' 3 | import { fork } from 'node:child_process' 4 | import { once } from 'node:events' 5 | 6 | const audioDrivers = sdl.info.drivers.audio.all 7 | let currentAudioDriver = sdl.info.drivers.audio.current 8 | let currentState = 'stopped' 9 | let currentTime = 0 10 | 11 | let audioProcess 12 | const restartAudioProcessWith = async (driver) => { 13 | audioProcess?.kill() 14 | audioProcess = fork('./audio-proc.js', { 15 | env: { 16 | ...process.env, 17 | SDL_AUDIODRIVER: driver, 18 | }, 19 | }) 20 | 21 | await once(audioProcess, 'message') 22 | 23 | audioProcess.on('message', (message) => { 24 | switch (message.type) { 25 | case 'end': { currentState = 'stopped' } break 26 | case 'time': { currentTime = message.time } break 27 | // No default 28 | } 29 | render() 30 | }) 31 | 32 | if (currentState === 'playing') { 33 | audioProcess.send({ type: 'play', time: currentTime }) 34 | } 35 | } 36 | await restartAudioProcessWith(currentAudioDriver) 37 | 38 | const buttons = [ 39 | { 40 | text: "play", 41 | rect: { x: 10, y: 30, w: 100, h: 20 }, 42 | fn () { 43 | if (currentState !== 'stopped') { return } 44 | currentState = 'playing' 45 | audioProcess.send({ type: 'play', time: currentTime }) 46 | }, 47 | }, 48 | { 49 | text: "stop", 50 | rect: { x: 120, y: 30, w: 100, h: 20 }, 51 | fn () { 52 | if (currentState !== 'playing') { return } 53 | currentState = 'stopped' 54 | audioProcess.send({ type: 'stop' }) 55 | }, 56 | }, 57 | ...audioDrivers.map((text, index) => ({ 58 | text, 59 | rect: { x: 10, y: 90 + index * 30, w: 210, h: 20 }, 60 | async fn () { 61 | currentAudioDriver = this.text 62 | await restartAudioProcessWith(currentAudioDriver) 63 | }, 64 | })), 65 | ] 66 | 67 | const width = 230 68 | const height = 90 + audioDrivers.length * 30 + 10 69 | 70 | const window = sdl.video.createWindow({ width, height }) 71 | const { pixelWidth, pixelHeight } = window 72 | 73 | Canvas.GlobalFonts.loadSystemFonts() 74 | const canvas = Canvas.createCanvas(pixelWidth, pixelHeight) 75 | const ctx = canvas.getContext('2d') 76 | 77 | const scaleX = pixelWidth / width 78 | const scaleY = pixelHeight / height 79 | ctx.scale(scaleX, scaleY) 80 | 81 | ctx.font = '14px "DejaVu Sans Mono"' 82 | ctx.strokeStyle = 'white' 83 | ctx.lineWidth = 2 * Math.min(scaleX, scaleY) 84 | ctx.textAlign = 'center' 85 | ctx.textBaseline = 'middle' 86 | 87 | const render = () => { 88 | ctx.fillStyle = 'black' 89 | ctx.fillRect(0, 0, width, height) 90 | ctx.fillStyle = 'white' 91 | 92 | const time = Math.round(currentTime / 1e3) 93 | const minutes = Math.floor(time / 60).toString().padStart(2, '0') 94 | const seconds = (time % 60).toString().padStart(2, '0') 95 | ctx.fillText(`State: ${currentState} ${minutes}:${seconds}`, width / 2, 15) 96 | 97 | ctx.fillText(`Using: ${currentAudioDriver}`, width / 2, 75) 98 | 99 | for (const { text, rect: { x, y, w, h } } of buttons) { 100 | ctx.strokeRect(x, y, w, h) 101 | ctx.fillText(text, x + w / 2, y + h / 2) 102 | } 103 | 104 | const buffer = Buffer.from(ctx.getImageData(0, 0, width, height).data) 105 | window.render(width, height, width * 4, 'rgba32', buffer) 106 | } 107 | 108 | render() 109 | 110 | let processing = false 111 | window.on('mouseButtonUp', async (event) => { 112 | if (processing) { return } 113 | processing = true 114 | 115 | if (event.button !== 1) { return } 116 | 117 | for (const button of buttons) { 118 | const { rect: { x, y, w, h } } = button 119 | if (x <= event.x && event.x <= x + w && y <= event.y && event.y <= y + h) { 120 | console.log(`Clicked: ${button.text}`) 121 | await button.fn() 122 | render() 123 | break 124 | } 125 | } 126 | 127 | processing = false 128 | }) 129 | 130 | window.on('close', () => { audioProcess?.kill() }) 131 | -------------------------------------------------------------------------------- /examples/17-audio-driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-audio-driver", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "@napi-rs/canvas": "*", 9 | "ffmpeg-static": "*" 10 | }, 11 | "trustedDependencies": [ 12 | "@kmamal/sdl", 13 | "@napi-rs/canvas", 14 | "ffmpeg-static" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/18-clipboard-mutator/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import Canvas from '@napi-rs/canvas' 3 | import { setTimeout } from 'timers/promises' 4 | 5 | const window = sdl.video.createWindow() 6 | const { pixelWidth: width, pixelHeight: height } = window 7 | const stride = width * 4 8 | 9 | Canvas.GlobalFonts.loadSystemFonts() 10 | const canvas = Canvas.createCanvas(width, height) 11 | const ctx = canvas.getContext('2d') 12 | 13 | ctx.font = '14px "DejaVu Sans Mono"' 14 | ctx.textAlign = 'left' 15 | ctx.textBaseline = 'top' 16 | 17 | let text = '' 18 | 19 | const render = () => { 20 | ctx.fillStyle = 'black' 21 | ctx.fillRect(0, 0, width, height) 22 | ctx.fillStyle = 'white' 23 | 24 | ctx.fillText(text, 0, 0) 25 | 26 | const buffer = Buffer.from(ctx.getImageData(0, 0, width, height).data) 27 | window.render(width, height, stride, 'rgba32', buffer) 28 | } 29 | 30 | const update = () => { 31 | text = sdl.clipboard.text 32 | render() 33 | } 34 | 35 | window.on('focus', update) 36 | sdl.clipboard.on('update', update) 37 | 38 | update() 39 | 40 | const maxUnicode = 0x10FFFF 41 | 42 | while (!window.destroyed) { 43 | const numChanges = Math.ceil(text.length / 100) 44 | 45 | for (let i = 0; i < numChanges; i++) { 46 | const chars = [ ...text ] 47 | const index = Math.floor(Math.random() * chars.length) 48 | const drift = Math.round(Math.random() * maxUnicode / 1000) 49 | const drifted = (chars[index].codePointAt(0) + drift) % maxUnicode 50 | const char = String.fromCodePoint(drifted) 51 | const before = chars.slice(0, index) 52 | const after = chars.slice(index + 1) 53 | 54 | text = [ ...before, char, ...after ].join('') 55 | } 56 | 57 | render() 58 | await setTimeout(100) 59 | } 60 | 61 | sdl.clipboard.off('update', update) 62 | -------------------------------------------------------------------------------- /examples/18-clipboard-mutator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-clipboard-mutator", 4 | "main": "index.js", 5 | "type": "module", 6 | "dependencies": { 7 | "@kmamal/sdl": "*", 8 | "@napi-rs/canvas": "*" 9 | }, 10 | "trustedDependencies": [ 11 | "@kmamal/sdl", 12 | "@napi-rs/canvas" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/19-packaging/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | name: ${{ matrix.platform.name }} 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | platform: 13 | - { name: 'Linux (x64)' ,os: ubuntu-latest } 14 | - { name: 'Windows (x64)' ,os: windows-latest } 15 | - { name: 'Mac (x64)' ,os: macos-latest } 16 | - { name: 'Mac (arm64)' ,os: macos-latest ,arch: arm64 } 17 | 18 | runs-on: ${{ matrix.platform.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - run: | 24 | npm i 25 | npm run package 26 | 27 | - uses: actions/upload-artifact@v4 28 | with: 29 | name: ${{ matrix.platform.name }}.zip 30 | path: dist/*.zip 31 | -------------------------------------------------------------------------------- /examples/19-packaging/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /examples/19-packaging/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/19-packaging/assets/image.png -------------------------------------------------------------------------------- /examples/19-packaging/assets/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/19-packaging/assets/video.mp4 -------------------------------------------------------------------------------- /examples/19-packaging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-packaging", 4 | "main": "src/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "package": "packager --include \"src/**/*.js\" --include \"assets/**/*\" --include \"package*.json\" --out-dir dist" 8 | }, 9 | "dependencies": { 10 | "@kmamal/sdl": "*", 11 | "ffmpeg-static": "*" 12 | }, 13 | "devDependencies": { 14 | "@kmamal/packager": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/19-packaging/src/ffmpeg.js: -------------------------------------------------------------------------------- 1 | import ffmpeg from 'ffmpeg-static' 2 | import { spawn } from 'node:child_process' 3 | import path from 'node:path' 4 | 5 | export const loadImage = async (filepath, options) => (await loadVideo(filepath, options))[0] 6 | 7 | export const loadVideo = async (filepath, { width, height, framerate }) => { 8 | const proc = spawn( 9 | ffmpeg, 10 | [ 11 | [ '-i', path.relative(process.cwd(), filepath).replaceAll(path.sep, path.posix.sep) ], 12 | '-filter:v', 13 | [ 14 | framerate && `fps=fps=${framerate}`, 15 | `scale=${width}:${height}`, 16 | 'format=pix_fmts=rgb24', 17 | ].filter(Boolean).join(','), 18 | [ '-f', 'rawvideo' ], 19 | '-', 20 | ].flat(), 21 | ) 22 | 23 | const frameSize = width * height * 3 24 | const frames = [] 25 | let chunks = [] 26 | let size = 0 27 | 28 | const append = (data) => { 29 | const end = frameSize - size 30 | const chunk = data.slice(0, end) 31 | chunks.push(chunk) 32 | size += chunk.length 33 | 34 | if (size === frameSize) { 35 | frames.push(Buffer.concat(chunks)) 36 | chunks = [] 37 | size = 0 38 | append(data.slice(end)) 39 | } 40 | } 41 | 42 | proc.stdout.on('data', append) 43 | 44 | const stderrChunks = [] 45 | proc.stderr.on('data', (chunk) => { 46 | stderrChunks.push(chunk.toString()) 47 | }) 48 | 49 | return await new Promise((resolve, reject) => { 50 | proc.on('close', (code) => { 51 | code 52 | ? reject(Object.assign(new Error(`exit code ${code}`), { 53 | stderr: stderrChunks.join(''), 54 | })) 55 | : resolve(frames) 56 | }) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /examples/19-packaging/src/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import path from 'node:path' 3 | import url from 'node:url' 4 | import { setTimeout } from 'node:timers/promises' 5 | import { loadImage, loadVideo } from './ffmpeg.js' 6 | 7 | const framerate = 25 8 | 9 | const window = sdl.video.createWindow() 10 | const { pixelWidth: width, pixelHeight: height } = window 11 | 12 | const dir = path.dirname(url.fileURLToPath(import.meta.url)) 13 | const [ image, video ] = await Promise.all([ 14 | loadImage(path.join(dir, '../assets/image.png'), { width, height }), 15 | loadVideo(path.join(dir, '../assets/video.mp4'), { width, height, framerate }), 16 | ]) 17 | 18 | let startTime = null 19 | let lastIndex = null 20 | 21 | window.on('mouseButtonUp', () => { 22 | startTime = startTime ? null : Date.now() 23 | render() 24 | }) 25 | 26 | const render = async () => { 27 | while (!window.destroyed) { 28 | // Render pause image 29 | if (!startTime) { 30 | window.render(width, height, width * 3, 'rgb24', image) 31 | return 32 | } 33 | 34 | // Render video 35 | { 36 | const time = (Date.now() - startTime) / 1e3 37 | const index = Math.floor(time * framerate) 38 | 39 | if (index === lastIndex) { continue } 40 | 41 | if (index >= video.length) { 42 | startTime = null 43 | lastIndex = null 44 | 45 | window.render(width, height, width * 3, 'rgb24', image) 46 | return 47 | } 48 | 49 | window.render(width, height, width * 3, 'rgb24', video[index]) 50 | lastIndex = index 51 | } 52 | 53 | await setTimeout(0) 54 | } 55 | } 56 | 57 | render() 58 | -------------------------------------------------------------------------------- /examples/20-pkg/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | name: ${{ matrix.platform.name }} 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | - { name: 'Linux (x64)' ,os: ubuntu-22.04 } 16 | - { name: 'Windows (x64)' ,os: windows-2022 } 17 | - { name: 'Mac (x64)' ,os: macos-12 } 18 | - { name: 'Mac (arm64)' ,os: macos-12 ,arch: arm64 } 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - run: npm i 26 | - run: npm run package 27 | 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | path: dist 31 | -------------------------------------------------------------------------------- /examples/20-pkg/assets/image.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/20-pkg/assets/image.bmp -------------------------------------------------------------------------------- /examples/20-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-pkg", 4 | "main": "src/index.js", 5 | "scripts": { 6 | "package": "cd scripts && node package.mjs" 7 | }, 8 | "dependencies": { 9 | "@kmamal/sdl": "*", 10 | "bmp-js": "*" 11 | }, 12 | "devDependencies": { 13 | "@yao-pkg/pkg": "*" 14 | }, 15 | "pkg": { 16 | "scripts": "src/**/*.js", 17 | "assets": [ "assets/**/*" ] 18 | }, 19 | "bin": "src/index.js" 20 | } 21 | -------------------------------------------------------------------------------- /examples/20-pkg/scripts/package.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | import url from 'node:url' 3 | import path from 'node:path' 4 | import fs from 'node:fs' 5 | 6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 7 | 8 | const majorVersion = process.version.slice(0, process.version.indexOf('.')) 9 | const target = `node${majorVersion}-${process.platform}-${process.arch}` 10 | const ext = process.platform === 'win32' ? '.exe' : '' 11 | 12 | execSync(`npx pkg --target ${target} --output dist/example${ext} .`, { 13 | cwd: path.resolve(__dirname, '..'), 14 | stdio: 'inherit', 15 | }) 16 | 17 | await fs.promises.cp( 18 | path.join(__dirname, '../node_modules/@kmamal/sdl/dist'), 19 | path.join(__dirname, '../dist'), 20 | { 21 | recursive: true, 22 | verbatimSymlinks: true, 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /examples/20-pkg/src/index.js: -------------------------------------------------------------------------------- 1 | const sdl = require('@kmamal/sdl') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const bmp = require('bmp-js') 5 | 6 | const bmpBuffer = fs.readFileSync(path.join(__dirname, '../assets/image.bmp')) 7 | const { width, height, data } = bmp.decode(bmpBuffer) 8 | 9 | const window = sdl.video.createWindow({ width, height }) 10 | window.render(width, height, width * 4, 'abgr32', data) 11 | -------------------------------------------------------------------------------- /examples/21-pkg-webpack/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | name: ${{ matrix.platform.name }} 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | - { name: 'Linux (x64)' ,os: ubuntu-22.04 } 16 | - { name: 'Windows (x64)' ,os: windows-2022 } 17 | - { name: 'Mac (x64)' ,os: macos-12 } 18 | - { name: 'Mac (arm64)' ,os: macos-12 ,arch: arm64 } 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - run: npm i 26 | - run: npm run package 27 | 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | path: dist 31 | -------------------------------------------------------------------------------- /examples/21-pkg-webpack/assets/image.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmamal/node-sdl/b229473660b16d5e6b8d6b13364b02acd193581e/examples/21-pkg-webpack/assets/image.bmp -------------------------------------------------------------------------------- /examples/21-pkg-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "sdl-example-pkg", 4 | "main": "src/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "package": "cd scripts && node package.mjs" 8 | }, 9 | "dependencies": { 10 | "@kmamal/sdl": "*", 11 | "bmp-js": "*" 12 | }, 13 | "devDependencies": { 14 | "@yao-pkg/pkg": "*", 15 | "webpack": "*", 16 | "webpack-cli": "*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/21-pkg-webpack/scripts/package.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | import url from 'node:url' 3 | import path from 'node:path' 4 | import fs from 'node:fs' 5 | import { rootCertificates } from 'node:tls' 6 | 7 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 8 | const rootDir = path.resolve(__dirname, '..') 9 | 10 | execSync('npx webpack', { 11 | cwd: rootDir, 12 | stdio: 'inherit', 13 | }) 14 | 15 | await fs.promises.writeFile( 16 | path.join(rootDir, 'build/package.json'), 17 | JSON.stringify({ 18 | main: "index.js", 19 | bin: "index.js", 20 | pkg: { 21 | scripts: "index.js", 22 | assets: [ "../assets/**/*" ], 23 | }, 24 | }, null, 2), 25 | ) 26 | 27 | const majorVersion = process.version.slice(0, process.version.indexOf('.')) 28 | const target = `node${majorVersion}-${process.platform}-${process.arch}` 29 | const ext = process.platform === 'win32' ? '.exe' : '' 30 | 31 | execSync(`npx pkg --target ${target} --output dist/example${ext} ./build/package.json`, { 32 | cwd: rootDir, 33 | stdio: 'inherit', 34 | }) 35 | 36 | await fs.promises.cp( 37 | path.join(__dirname, '../node_modules/@kmamal/sdl/dist'), 38 | path.join(__dirname, '../dist'), 39 | { 40 | recursive: true, 41 | verbatimSymlinks: true, 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /examples/21-pkg-webpack/src/bmp.js: -------------------------------------------------------------------------------- 1 | import bmp from 'bmp-js' 2 | import fs from 'node:fs' 3 | 4 | export const loadBmp = async (filePath) => { 5 | const bmpBuffer = await fs.promises.readFile(filePath) 6 | return bmp.decode(bmpBuffer) 7 | } 8 | -------------------------------------------------------------------------------- /examples/21-pkg-webpack/src/index.js: -------------------------------------------------------------------------------- 1 | import sdl from '@kmamal/sdl' 2 | import path from 'node:path' 3 | import url from 'node:url' 4 | import { loadBmp } from './bmp.js' 5 | 6 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 7 | 8 | const imagePath = path.join(__dirname, '../assets/image.bmp') 9 | const { width, height, data } = await loadBmp(imagePath) 10 | 11 | const window = sdl.video.createWindow({ width, height }) 12 | window.render(width, height, width * 4, 'abgr32', data) 13 | -------------------------------------------------------------------------------- /examples/21-pkg-webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import url from 'node:url' 3 | 4 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 5 | 6 | export default { 7 | mode: 'development', 8 | entry: './src/index.js', 9 | output: { 10 | path: path.join(__dirname, 'build'), 11 | filename: 'index.js', 12 | }, 13 | target: 'node', 14 | externals: [ 15 | ({ request }, callback) => { 16 | if (request.endsWith('/sdl.node')) { 17 | return callback(null, `commonjs ./sdl.node`) 18 | } 19 | 20 | callback() 21 | }, 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.11.7", 3 | "name": "@kmamal/sdl", 4 | "description": "SDL bindings for Node.js", 5 | "keywords": [ 6 | "sdl", 7 | "sdl2", 8 | "gui", 9 | "desktop", 10 | "canvas", 11 | "webgl", 12 | "opengl", 13 | "webgpu", 14 | "window", 15 | "screen", 16 | "video", 17 | "input", 18 | "mouse", 19 | "keyboard", 20 | "joystick", 21 | "controller", 22 | "gamepad", 23 | "rumble", 24 | "audio", 25 | "sound", 26 | "speaker", 27 | "microphone", 28 | "mic", 29 | "clipboard", 30 | "power", 31 | "battery", 32 | "game" 33 | ], 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "git+ssh://git@github.com/kmamal/node-sdl.git" 38 | }, 39 | "main": "./src/javascript/index.js", 40 | "types": "./src/types/index.d.ts", 41 | "exports": { 42 | ".": { 43 | "types": "./src/types/index.d.ts", 44 | "default": "./src/javascript/index.js" 45 | }, 46 | "./helpers": { 47 | "types": "./src/types/helpers.d.ts", 48 | "default": "./src/javascript/helpers.js" 49 | } 50 | }, 51 | "gypfile": true, 52 | "scripts": { 53 | "test": "npx @kmamal/testing", 54 | "install": "cd scripts && node install.mjs", 55 | "download-release": "cd scripts && node download-release.mjs", 56 | "download-sdl": "cd scripts && node download-sdl.mjs", 57 | "build": "cd scripts && node build.mjs", 58 | "upload-release": "cd scripts && node upload-release.mjs", 59 | "release": "cd scripts && node release.mjs", 60 | "clean": "cd scripts && node clean.mjs" 61 | }, 62 | "dependencies": { 63 | "node-addon-api": "^8.3.1", 64 | "node-gyp": "^11.1.0", 65 | "tar": "^7.4.3" 66 | }, 67 | "sdl": { 68 | "owner": "kmamal", 69 | "repo": "build-sdl", 70 | "version": "2.32.2" 71 | }, 72 | "devDependencies": { 73 | "@kmamal/evdev": "^0.0.2", 74 | "@kmamal/testing": "^0.0.31", 75 | "@types/node": "^22.13.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scripts/build-sdl.mjs: -------------------------------------------------------------------------------- 1 | import Fs from 'node:fs' 2 | import Path from 'node:path' 3 | import { once } from 'node:events' 4 | import C from './util/common.js' 5 | import { fetch } from './util/fetch.js' 6 | import * as Tar from 'tar' 7 | 8 | const url = `https://github.com/${C.sdl.owner}/${C.sdl.repo}/archive/refs/tags/v${C.sdl.version}.tar.gz` 9 | 10 | console.log("fetch", url) 11 | const response = await fetch(url) 12 | 13 | console.log("unpack to", C.dir.sdl) 14 | await Fs.promises.rm(C.dir.sdl, { recursive: true }).catch(() => {}) 15 | await Fs.promises.mkdir(C.dir.sdl, { recursive: true }) 16 | const tar = Tar.extract({ gzip: true, C: C.dir.sdl }) 17 | response.stream().pipe(tar) 18 | await once(tar, 'finish') 19 | 20 | const sdlRoot = Path.join(C.dir.sdl, `${C.sdl.repo}-${C.sdl.version}`) 21 | await import(Path.join(sdlRoot, '/scripts/build.mjs')) 22 | const sdlDist = Path.join(sdlRoot, 'dist') 23 | await Promise.all([ 24 | Fs.promises.cp( 25 | Path.join(sdlDist, 'include'), 26 | Path.join(C.dir.sdl, 'include'), 27 | { recursive: true }, 28 | ), 29 | Fs.promises.cp( 30 | Path.join(sdlDist, 'lib'), 31 | Path.join(C.dir.sdl, 'lib'), 32 | { recursive: true }, 33 | ), 34 | ]) 35 | -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import Fs from 'node:fs' 2 | import Path from 'node:path' 3 | import { execSync } from 'node:child_process' 4 | import C from './util/common.js' 5 | 6 | await Promise.all([ 7 | C.dir.build, 8 | C.dir.dist, 9 | C.dir.publish, 10 | ].map(async (dir) => { 11 | await Fs.promises.rm(dir, { recursive: true }).catch(() => {}) 12 | })) 13 | 14 | console.log("build in", C.dir.build) 15 | const SDL_INC = Path.join(C.dir.sdl, 'include') 16 | const SDL_LIB = Path.join(C.dir.sdl, 'lib') 17 | 18 | let archFlag = '' 19 | if (process.env.CROSS_COMPILE_ARCH) { 20 | archFlag = `--arch ${process.env.CROSS_COMPILE_ARCH}` 21 | } 22 | 23 | let parallelFlag = '-j max' 24 | if (process.env.NO_PARALLEL) { 25 | parallelFlag = '' 26 | } 27 | 28 | process.chdir(C.dir.root) 29 | execSync(`npx -y node-gyp rebuild ${archFlag} ${parallelFlag} --verbose`, { 30 | stdio: 'inherit', 31 | env: { 32 | ...process.env, 33 | SDL_INC, 34 | SDL_LIB, 35 | }, 36 | }) 37 | 38 | console.log("install to", C.dir.dist) 39 | await Fs.promises.rm(C.dir.dist, { recursive: true }).catch(() => {}) 40 | await Fs.promises.mkdir(C.dir.dist, { recursive: true }) 41 | await Promise.all([ 42 | Fs.promises.cp( 43 | Path.join(C.dir.build, 'Release/sdl.node'), 44 | Path.join(C.dir.dist, 'sdl.node'), 45 | ), 46 | (async () => { 47 | const libs = await Fs.promises.readdir(SDL_LIB) 48 | await Promise.all(libs.map(async (name) => { 49 | if (C.platform === 'win32' && name !== 'SDL2.dll') { return } 50 | await Fs.promises.cp( 51 | Path.join(SDL_LIB, name), 52 | Path.join(C.dir.dist, name), 53 | { verbatimSymlinks: true }, 54 | ) 55 | })) 56 | })(), 57 | ]) 58 | 59 | // Strip binaries on linux 60 | if (C.platform === 'linux') { 61 | execSync(`strip -s ${Path.join(C.dir.dist, 'sdl.node')}`) 62 | } 63 | -------------------------------------------------------------------------------- /scripts/clean.mjs: -------------------------------------------------------------------------------- 1 | import Fs from 'node:fs' 2 | import Path from 'node:path' 3 | import C from './util/common.js' 4 | 5 | const dirs = [ 6 | Path.join(C.dir.root, 'node_modules'), 7 | C.dir.sdl, 8 | C.dir.build, 9 | C.dir.dist, 10 | C.dir.publish, 11 | ] 12 | 13 | console.log("delete") 14 | await Promise.all(dirs.map(async (dir) => { 15 | console.log(" ", dir) 16 | await Fs.promises.rm(dir, { recursive: true }).catch(() => {}) 17 | })) 18 | -------------------------------------------------------------------------------- /scripts/download-release.mjs: -------------------------------------------------------------------------------- 1 | import Fs from 'node:fs' 2 | import { once } from 'node:events' 3 | import C from './util/common.js' 4 | import { fetch } from './util/fetch.js' 5 | import * as Tar from 'tar' 6 | 7 | const url = `https://github.com/${C.owner}/${C.repo}/releases/download/v${C.version}/${C.assetName}` 8 | 9 | console.log("fetch", url) 10 | const response = await fetch(url) 11 | 12 | console.log("unpack to", C.dir.dist) 13 | await Fs.promises.rm(C.dir.dist, { recursive: true }).catch(() => {}) 14 | await Fs.promises.mkdir(C.dir.dist, { recursive: true }) 15 | const tar = Tar.extract({ gzip: true, C: C.dir.dist }) 16 | response.stream().pipe(tar) 17 | await once(tar, 'finish') 18 | -------------------------------------------------------------------------------- /scripts/download-sdl.mjs: -------------------------------------------------------------------------------- 1 | import Fs from 'node:fs' 2 | import { once } from 'node:events' 3 | import C from './util/common.js' 4 | import { fetch } from './util/fetch.js' 5 | import * as Tar from 'tar' 6 | 7 | const url = `https://github.com/${C.sdl.owner}/${C.sdl.repo}/releases/download/v${C.sdl.version}/${C.sdl.assetName}` 8 | 9 | console.log("fetch", url) 10 | try { 11 | const response = await fetch(url) 12 | 13 | 14 | console.log("unpack to", C.dir.sdl) 15 | await Fs.promises.rm(C.dir.sdl, { recursive: true }).catch(() => {}) 16 | await Fs.promises.mkdir(C.dir.sdl, { recursive: true }) 17 | const tar = Tar.extract({ gzip: true, C: C.dir.sdl }) 18 | response.stream().pipe(tar) 19 | await once(tar, 'finish') 20 | } catch (error) { 21 | console.log(error) 22 | throw error 23 | } 24 | -------------------------------------------------------------------------------- /scripts/install-deps-mac.sh: -------------------------------------------------------------------------------- 1 | 2 | brew install xquartz 3 | -------------------------------------------------------------------------------- /scripts/install-deps-raspbian.sh: -------------------------------------------------------------------------------- 1 | 2 | apt-get install -y \ 3 | build-essential \ 4 | libx11-dev 5 | -------------------------------------------------------------------------------- /scripts/install-deps-ubuntu.sh: -------------------------------------------------------------------------------- 1 | 2 | apt-get install -y \ 3 | build-essential \ 4 | libx11-dev 5 | -------------------------------------------------------------------------------- /scripts/install.mjs: -------------------------------------------------------------------------------- 1 | 2 | if (!process.env.NODE_SDL_FROM_SOURCE) { 3 | try { 4 | await import('./download-release.mjs') 5 | process.exit(0) 6 | } catch (_) { 7 | console.log("failed to download release") 8 | } 9 | } else { 10 | console.log("skip download and build from source") 11 | } 12 | 13 | try { 14 | await import('./download-sdl.mjs') 15 | } catch (_) { 16 | console.log("failed to download sdl") 17 | await import('./build-sdl.mjs') 18 | } 19 | 20 | await import('./build.mjs') 21 | -------------------------------------------------------------------------------- /scripts/release-rpi-qemu.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eEu -o pipefail 4 | set -x 5 | 6 | OS="raspios_lite_arm64" 7 | DATE="2023-05-03" 8 | IMAGE="$DATE-raspios-bullseye-arm64-lite.img" 9 | MOUNT="/mnt/raspbian" 10 | 11 | DIR="/tmp/node-sdl-rpi-qemu" 12 | rm -rf "$DIR" || true 13 | mkdir -p "$DIR" 14 | cd "$DIR" 15 | echo "Working in $DIR" 16 | 17 | 18 | sudo apt-get update 19 | sudo apt-get install -y jq xz-utils qemu-system-arm expect 20 | 21 | CACHED="/home/$USER/Desktop/$IMAGE.xz" 22 | if test -f "$CACHED"; then 23 | cp "$CACHED" . 24 | else 25 | wget --progress=dot:giga "https://downloads.raspberrypi.org/$OS/images/$OS-$DATE/$IMAGE.xz" 26 | fi 27 | unxz "$IMAGE.xz" 28 | 29 | qemu-img resize "$IMAGE" 4G 30 | 31 | expect -f - <<- EOF 32 | set timeout -1 33 | # exp_internal 1 34 | 35 | spawn fdisk {$IMAGE} 36 | 37 | expect -exact {Command (m for help): } 38 | send -- [string cat {d} "\r"] 39 | 40 | expect -exact {Partition number (1,2, default 2): } 41 | send -- [string cat {} "\r"] 42 | 43 | expect -exact {Command (m for help): } 44 | send -- [string cat {n} "\r"] 45 | 46 | expect -exact {Select (default p): } 47 | send -- [string cat {} "\r"] 48 | 49 | expect -exact {Partition number (2-4, default 2): } 50 | send -- [string cat {} "\r"] 51 | 52 | expect -exact {First sector (2048-8388607, default 2048): } 53 | send -- [string cat {532480} "\r"] 54 | 55 | expect -exact {(532480-8388607, default 8388607): } 56 | send -- [string cat {} "\r"] 57 | 58 | expect -exact {Do you want to remove the signature?} 59 | send -- [string cat {N} "\r"] 60 | 61 | expect -exact {Command (m for help): } 62 | send -- [string cat {w} "\r"] 63 | 64 | expect eof 65 | EOF 66 | 67 | LOOP="$(sudo losetup -f)" 68 | sudo losetup -P "$LOOP" "$IMAGE" 69 | sudo e2fsck -fa "${LOOP}p2" || true 70 | sudo resize2fs "${LOOP}p2" 71 | sudo losetup -d "${LOOP}" 72 | 73 | sudo mkdir -p "$MOUNT" 74 | sudo mount -o loop,offset=4194304 "$IMAGE" "$MOUNT" 75 | 76 | printf 'pi:$6$c70VpvPsVNCG0YR5$l5vWWLsLko9Kj65gcQ8qvMkuOoRkEagI90qi3F/Y7rm8eNYZHW8CY6BOIKwMH7a3YYzZYL90zf304cAHLFaZE0\n' | sudo tee "$MOUNT/userconf" > /dev/null 77 | 78 | NODE_VERSION=$(curl -fsSL https://nodejs.org/download/release/index.json | jq -r '.[] | select(.lts) | .version' | sort -Vr | head -n1) 79 | NODE_NAME="node-$NODE_VERSION-linux-arm64" 80 | NODE_URL="https://nodejs.org/download/release/$NODE_VERSION/$NODE_NAME.tar.gz" 81 | REPO_URL="https://github.com/kmamal/node-sdl/archive/refs/heads/master.tar.gz" 82 | 83 | expect -f - <<- EOF 84 | set timeout -1 85 | # exp_internal 1 86 | 87 | spawn qemu-system-aarch64 \ 88 | -machine raspi3b \ 89 | -cpu cortex-a72 \ 90 | -dtb "$MOUNT/bcm2710-rpi-3-b-plus.dtb" \ 91 | -m 1G -smp 4 -nographic -monitor none -serial stdio \ 92 | -kernel "$MOUNT/kernel8.img" \ 93 | -sd "$IMAGE" \ 94 | -netdev user,id=net0 \ 95 | -device usb-net,netdev=net0 \ 96 | -append "rw earlyprintk loglevel=8 console=ttyAMA0,115200 dwc_otg.lpm_enable=0 root=/dev/mmcblk0p2 rootdelay=1" 97 | 98 | while {true} { 99 | expect -exact {raspberrypi login: } 100 | send -- [string cat {pi} "\r"] 101 | 102 | expect -exact {Password: } 103 | send -- [string cat {raspberry} "\r"] 104 | 105 | expect { 106 | {pi@raspberrypi} { 107 | send -- "\r" 108 | break 109 | } 110 | {Login incorrect} {} 111 | } 112 | } 113 | 114 | expect -exact {pi@raspberrypi} 115 | send -- [string cat {curl -fsSL $NODE_URL | tar vxzf -} "\r"] 116 | 117 | expect -exact {pi@raspberrypi} 118 | send -- [string cat {export PATH="\$(pwd)/$NODE_NAME/bin:\$PATH"} "\r"] 119 | 120 | expect -exact {pi@raspberrypi} 121 | send -- [string cat {curl -fsSL $REPO_URL | tar vxzf -} "\r"] 122 | 123 | expect -exact {pi@raspberrypi} 124 | send -- [string cat {cd node-sdl-master} "\r"] 125 | 126 | expect -exact {pi@raspberrypi} 127 | send -- [string cat {sudo apt-get update && sudo ./scripts/install-deps-raspbian.sh} "\r"] 128 | 129 | expect -exact {pi@raspberrypi} 130 | send -- [string cat {GITHUB_TOKEN="$GITHUB_TOKEN" NO_PARALLEL=1 npm run release} "\r"] 131 | 132 | expect -exact {pi@raspberrypi} 133 | send -- [string cat {sudo shutdown -h now} "\r"] 134 | 135 | expect eof 136 | EOF 137 | 138 | # debug 139 | # sudo mount -o loop,offset=272629760 "$IMAGE" "$MOUNT" 140 | -------------------------------------------------------------------------------- /scripts/release.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | 3 | await import('./clean.mjs') 4 | 5 | execSync('npm install', { 6 | stdio: 'inherit', 7 | env: { 8 | ...process.env, 9 | NODE_SDL_FROM_SOURCE: 1, 10 | }, 11 | }) 12 | 13 | await import('./upload-release.mjs') 14 | -------------------------------------------------------------------------------- /scripts/upload-release.mjs: -------------------------------------------------------------------------------- 1 | import Fs from 'node:fs' 2 | import Path from 'node:path' 3 | import C from './util/common.js' 4 | import { fetch } from './util/fetch.js' 5 | import * as Tar from 'tar' 6 | 7 | const commonHeaders = { 8 | "Accept": 'application/vnd.github+json', 9 | "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`, 10 | 'User-Agent': `@${C.owner}/${C.repo}@${C.version}`, 11 | } 12 | 13 | let response 14 | 15 | getRelease: { 16 | console.log("get release", C.version) 17 | 18 | try { 19 | response = await fetch( 20 | `https://api.github.com/repos/${C.owner}/${C.repo}/releases/tags/v${C.version}`, 21 | { headers: commonHeaders }, 22 | ) 23 | console.log("release exists", C.version) 24 | break getRelease 25 | } catch (error) { 26 | console.log(error.message) 27 | } 28 | 29 | console.log("create release", C.version) 30 | 31 | response = await fetch( 32 | `https://api.github.com/repos/${C.owner}/${C.repo}/releases`, 33 | { 34 | method: 'POST', 35 | headers: commonHeaders, 36 | body: JSON.stringify({ 37 | tag_name: `v${C.version}`, // eslint-disable-line camelcase 38 | name: `v${C.version}`, 39 | prerelease: C.isPrerelease, 40 | make_latest: `${!C.isPrerelease}`, // eslint-disable-line camelcase 41 | }), 42 | }, 43 | ) 44 | } 45 | const releaseId = (await response.json()).id 46 | 47 | console.log("create archive", C.assetName) 48 | await Fs.promises.rm(C.dir.publish, { recursive: true }).catch(() => {}) 49 | await Fs.promises.mkdir(C.dir.publish, { recursive: true }) 50 | const assetPath = Path.join(C.dir.publish, C.assetName) 51 | 52 | process.chdir(C.dir.dist) 53 | await Tar.create( 54 | { gzip: true, file: assetPath }, 55 | await Fs.promises.readdir('.'), 56 | ) 57 | const buffer = await Fs.promises.readFile(assetPath) 58 | 59 | response = await fetch( 60 | `https://api.github.com/repos/${C.owner}/${C.repo}/releases/${releaseId}/assets`, 61 | { headers: commonHeaders }, 62 | ) 63 | 64 | const list = await response.json() 65 | const asset = list.find((x) => x.name === C.assetName) 66 | if (asset) { 67 | console.log("delete asset", C.assetName) 68 | await fetch( 69 | `https://api.github.com/repos/${C.owner}/${C.repo}/releases/assets/${asset.id}`, 70 | { 71 | method: 'DELETE', 72 | headers: commonHeaders, 73 | }, 74 | ) 75 | } 76 | 77 | console.log("upload", C.assetName) 78 | await fetch( 79 | `https://uploads.github.com/repos/${C.owner}/${C.repo}/releases/${releaseId}/assets?name=${C.assetName}`, 80 | { 81 | method: 'POST', 82 | headers: { 83 | ...commonHeaders, 84 | 'Content-Type': 'application/gzip', 85 | }, 86 | body: buffer, 87 | }, 88 | ) 89 | -------------------------------------------------------------------------------- /scripts/util/common.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs') 2 | const Path = require('path') 3 | 4 | const dir = {} 5 | dir.root = Path.join(__dirname, '../..') 6 | dir.sdl = Path.join(dir.root, 'sdl') 7 | dir.build = Path.join(dir.root, 'build') 8 | dir.dist = Path.join(dir.root, 'dist') 9 | dir.publish = Path.join(dir.root, 'publish') 10 | 11 | const pkgPath = Path.join(dir.root, 'package.json') 12 | const pkg = JSON.parse(Fs.readFileSync(pkgPath).toString()) 13 | const version = pkg.version 14 | const isPrerelease = version.includes('-') 15 | const [ , owner, repo ] = pkg.repository.url.match(/([^/:]+)\/([^/]+).git$/u) 16 | 17 | const { platform, arch } = process 18 | const targetArch = process.env.CROSS_COMPILE_ARCH || arch 19 | const assetName = `sdl.node-v${version}-${platform}-${targetArch}.tar.gz` 20 | 21 | const sdl = pkg.sdl 22 | sdl.assetName = `SDL-v${sdl.version}-${platform}-${targetArch}.tar.gz` 23 | 24 | module.exports = { 25 | dir, 26 | version, 27 | isPrerelease, 28 | owner, 29 | repo, 30 | platform, 31 | arch, 32 | targetArch, 33 | assetName, 34 | sdl, 35 | } 36 | -------------------------------------------------------------------------------- /scripts/util/fetch.js: -------------------------------------------------------------------------------- 1 | 2 | const _consume = async (stream) => { 3 | const chunks = [] 4 | for await (const chunk of stream) { 5 | chunks.push(chunk) 6 | } 7 | return Buffer.concat(chunks) 8 | } 9 | 10 | const fetch = async (_url, options = {}) => { 11 | let url = _url 12 | let { 13 | maxRedirect = 5, 14 | body = null, 15 | } = options 16 | 17 | const { protocol } = new URL(url) 18 | const lib = require(protocol.slice(0, -1)) 19 | 20 | const evalPromise = (resolve, reject) => { 21 | const request = lib.request(url, options) 22 | .on('error', reject) 23 | .on('response', resolve) 24 | 25 | if (!body) { 26 | request.end() 27 | return 28 | } 29 | 30 | if (body?.length) { 31 | if (!Buffer.isBuffer(body)) { body = Buffer.from(body) } 32 | body = Promise.resolve(body) 33 | } else { 34 | body = _consume(body) 35 | } 36 | 37 | body.then((buffer) => { 38 | request.setHeader('Content-Length', buffer.length) 39 | request.end(buffer) 40 | }) 41 | } 42 | 43 | for (;;) { 44 | const response = await new Promise(evalPromise) 45 | 46 | const { statusCode } = response 47 | 48 | if (300 <= statusCode && statusCode < 400) { 49 | if (maxRedirect-- > 0) { 50 | url = response.headers.location 51 | continue 52 | } 53 | } 54 | 55 | if (!(200 <= statusCode && statusCode < 300)) { 56 | let responseBody 57 | try { responseBody = (await _consume(response)).toString() } catch (_) {} 58 | try { responseBody = JSON.parse(responseBody) } catch (_) {} 59 | throw Object.assign(new Error(`bad status code ${statusCode}`), { 60 | statusCode, 61 | responseBody, 62 | }) 63 | } 64 | 65 | return { 66 | stream () { return response }, 67 | async buffer () { 68 | return await _consume(response) 69 | }, 70 | async text () { 71 | const buffer = await this.buffer() 72 | return buffer.toString() 73 | }, 74 | async json () { 75 | const text = await this.text() 76 | return JSON.parse(text) 77 | }, 78 | } 79 | } 80 | } 81 | 82 | module.exports = { fetch } 83 | -------------------------------------------------------------------------------- /src/javascript/audio/audio-instance.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const Enums = require('../enums') 4 | const { EventsViaPoll } = require('../events/events-via-poll') 5 | const { AudioFormatHelpers } = require('./format-helpers') 6 | 7 | const validEvents = [ 'close' ] 8 | 9 | class AudioInstance extends EventsViaPoll { 10 | constructor (device, options) { 11 | super(validEvents) 12 | 13 | const { name, type } = device 14 | 15 | const { 16 | channels = 1, 17 | frequency = 48000, 18 | format = 'f32', 19 | buffered = 4096, 20 | } = options 21 | 22 | if (name !== undefined && name !== null && typeof name !== 'string') { throw Object.assign(new Error("device.name must be a string"), { name }) } 23 | // We already tested device.type in sdl.audio.openDevice() 24 | if (!Number.isInteger(channels)) { throw Object.assign(new Error("channels must be an integer"), { channels }) } 25 | if (![ 1, 2, 4, 6 ].includes(channels)) { throw Object.assign(new Error("invalid channels"), { channels }) } 26 | if (!Number.isInteger(frequency)) { throw Object.assign(new Error("frequency must be an integer"), { frequency }) } 27 | if (frequency <= 0) { throw Object.assign(new Error("invalid frequency"), { frequency }) } 28 | if (typeof format !== 'string') { throw Object.assign(new Error("format must be a string"), { format }) } 29 | if (!Number.isInteger(buffered)) { throw Object.assign(new Error("buffered must be an integer"), { buffered }) } 30 | if (buffered <= 0) { throw Object.assign(new Error("invalid buffered"), { buffered }) } 31 | if (buffered !== 2 ** (32 - Math.clz32(buffered) - 1)) { throw Object.assign(new Error("invalid buffered"), { buffered }) } 32 | 33 | const _format = Enums.audioFormat[format] 34 | if (_format === undefined) { throw Object.assign(new Error("invalid format"), { format }) } 35 | 36 | this._id = Bindings.audio_open(name ?? null, type === 'recording', frequency, _format, channels, buffered) 37 | 38 | this._device = device 39 | this._name = name 40 | this._buffered = buffered 41 | this._channels = channels 42 | this._format = format 43 | this._frequency = frequency 44 | 45 | this._playing = false 46 | this._closed = false 47 | 48 | const helper = AudioFormatHelpers[this._format] 49 | this._bytesPerSample = helper.bytesPerSample 50 | this._minSampleValue = helper.minSampleValue 51 | this._maxSampleValue = helper.maxSampleValue 52 | this._zeroSampleValue = helper.zeroSampleValue 53 | this._reader = helper.reader 54 | this._writer = helper.writer 55 | 56 | Globals.audioInstances.set(this._id, this) 57 | } 58 | 59 | get id () { return this._id } 60 | get device () { return this._device } 61 | get name () { return this._name } 62 | 63 | get channels () { return this._channels } 64 | get frequency () { return this._frequency } 65 | 66 | get format () { return this._format } 67 | get bytesPerSample () { return this._bytesPerSample } 68 | get minSampleValue () { return this._minSampleValue } 69 | get maxSampleValue () { return this._maxSampleValue } 70 | get zeroSampleValue () { return this._zeroSampleValue } 71 | 72 | readSample (buffer, offset) { 73 | return this._reader.call(buffer, offset) 74 | } 75 | 76 | writeSample (buffer, value, offset) { 77 | return this._writer.call(buffer, value, offset) 78 | } 79 | 80 | get buffered () { return this._buffered } 81 | 82 | get playing () { return this._playing } 83 | play (play = true) { 84 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._id }) } 85 | 86 | if (typeof play !== 'boolean') { throw Object.assign(new Error("play must be a boolean"), { play }) } 87 | 88 | Bindings.audio_play(this._id, play) 89 | 90 | this._playing = play 91 | } 92 | 93 | pause () { 94 | this.play(false) 95 | } 96 | 97 | get queued () { 98 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._id }) } 99 | 100 | return Bindings.audio_getQueueSize(this._id) 101 | } 102 | 103 | clearQueue () { 104 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._id }) } 105 | 106 | Bindings.audio_clearQueue(this._id) 107 | } 108 | 109 | get closed () { return this._closed } 110 | close () { 111 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._id }) } 112 | 113 | this.emit('close') 114 | this.removeAllListeners() 115 | this._closed = true 116 | 117 | Bindings.audio_close(this._id) 118 | 119 | Globals.audioInstances.delete(this._id) 120 | } 121 | } 122 | 123 | module.exports = { AudioInstance } 124 | -------------------------------------------------------------------------------- /src/javascript/audio/audio-playback-instance.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('../bindings') 2 | const { AudioInstance } = require('./audio-instance') 3 | const { resetTimeout } = require('./prevent-exit') 4 | 5 | class AudioPlaybackInstance extends AudioInstance { 6 | enqueue (buffer, numBytes = buffer.length) { 7 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._id }) } 8 | 9 | if (!(buffer instanceof Buffer)) { throw Object.assign(new Error("buffer must be a Buffer"), { buffer }) } 10 | if (!Number.isInteger(numBytes)) { throw Object.assign(new Error("numBytes must be an integer"), { numBytes }) } 11 | if (numBytes <= 0) { throw Object.assign(new Error("invalid numBytes"), { numBytes }) } 12 | if (buffer.length < numBytes) { throw Object.assign(new Error("buffer is smaller than expected"), { buffer, numBytes }) } 13 | 14 | Bindings.audio_enqueue(this._id, buffer, numBytes) 15 | } 16 | 17 | clearQueue () { 18 | super.clearQueue() 19 | resetTimeout() 20 | } 21 | } 22 | 23 | module.exports = { AudioPlaybackInstance } 24 | -------------------------------------------------------------------------------- /src/javascript/audio/audio-recording-instance.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('../bindings') 2 | const { AudioInstance } = require('./audio-instance') 3 | 4 | class AudioRecordingInstance extends AudioInstance { 5 | dequeue (buffer, numBytes = buffer.length) { 6 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._id }) } 7 | 8 | if (!(buffer instanceof Buffer)) { throw Object.assign(new Error("buffer must be a Buffer"), { buffer }) } 9 | if (!Number.isInteger(numBytes)) { throw Object.assign(new Error("numBytes must be an integer"), { numBytes }) } 10 | if (numBytes <= 0) { throw Object.assign(new Error("invalid numBytes"), { numBytes }) } 11 | if (buffer.length < numBytes) { throw Object.assign(new Error("buffer is smaller than expected"), { buffer, numBytes }) } 12 | 13 | return Bindings.audio_dequeue(this._id, buffer, numBytes) 14 | } 15 | } 16 | 17 | module.exports = { AudioRecordingInstance } 18 | -------------------------------------------------------------------------------- /src/javascript/audio/device.js: -------------------------------------------------------------------------------- 1 | 2 | const compare = (a, b) => { 3 | const { name: aName } = a 4 | const { name: bName } = b 5 | if (aName === bName) { return 0 } 6 | return a.name < b.name ? -1 : 1 7 | } 8 | 9 | module.exports = { compare } 10 | -------------------------------------------------------------------------------- /src/javascript/audio/format-helpers.js: -------------------------------------------------------------------------------- 1 | const Os = require('os') 2 | 3 | const signedLimits = (bits) => ({ 4 | bytesPerSample: bits / 8, 5 | minSampleValue: -(2 ** (bits - 1)), 6 | zeroSampleValue: 0, 7 | maxSampleValue: (2 ** (bits - 1)) - 1, 8 | }) 9 | 10 | const unsignedLimits = (bits) => ({ 11 | bytesPerSample: bits / 8, 12 | minSampleValue: 0, 13 | zeroSampleValue: (2 ** (bits - 1)) - 1, 14 | maxSampleValue: (2 ** bits) - 1, 15 | }) 16 | 17 | const floatLimits = { 18 | bytesPerSample: 4, 19 | minSampleValue: -1, 20 | zeroSampleValue: 0, 21 | maxSampleValue: 1, 22 | } 23 | 24 | const AudioFormatHelpers = { 25 | s8: { 26 | reader: Buffer.prototype.readInt8, 27 | writer: Buffer.prototype.writeInt8, 28 | ...signedLimits(8), 29 | }, 30 | u8: { 31 | reader: Buffer.prototype.readUInt8, 32 | writer: Buffer.prototype.writeUInt8, 33 | ...unsignedLimits(8), 34 | }, 35 | s16lsb: { 36 | reader: Buffer.prototype.readInt16LE, 37 | writer: Buffer.prototype.writeInt16LE, 38 | ...signedLimits(16), 39 | }, 40 | s16msb: { 41 | reader: Buffer.prototype.readInt16BE, 42 | writer: Buffer.prototype.writeInt16BE, 43 | ...signedLimits(16), 44 | }, 45 | u16lsb: { 46 | reader: Buffer.prototype.readUInt16LE, 47 | writer: Buffer.prototype.writeUInt16LE, 48 | ...unsignedLimits(16), 49 | }, 50 | u16msb: { 51 | reader: Buffer.prototype.readUInt16BE, 52 | writer: Buffer.prototype.writeUInt16BE, 53 | ...unsignedLimits(16), 54 | }, 55 | s32lsb: { 56 | reader: Buffer.prototype.readInt32LE, 57 | writer: Buffer.prototype.writeInt32LE, 58 | ...signedLimits(32), 59 | }, 60 | s32msb: { 61 | reader: Buffer.prototype.readInt32BE, 62 | writer: Buffer.prototype.writeInt32BE, 63 | ...signedLimits(32), 64 | }, 65 | f32lsb: { 66 | reader: Buffer.prototype.readFloatLE, 67 | writer: Buffer.prototype.writeFloatLE, 68 | ...floatLimits, 69 | }, 70 | f32msb: { 71 | reader: Buffer.prototype.readFloatBE, 72 | writer: Buffer.prototype.writeFloatBE, 73 | ...floatLimits, 74 | }, 75 | } 76 | 77 | AudioFormatHelpers.s16 = AudioFormatHelpers.s16lsb 78 | AudioFormatHelpers.u16 = AudioFormatHelpers.u16lsb 79 | AudioFormatHelpers.s32 = AudioFormatHelpers.s32lsb 80 | AudioFormatHelpers.f32 = AudioFormatHelpers.f32lsb 81 | 82 | if (Os.endianness() === 'LE') { 83 | AudioFormatHelpers.s16sys = AudioFormatHelpers.s16lsb 84 | AudioFormatHelpers.u16sys = AudioFormatHelpers.u16lsb 85 | AudioFormatHelpers.s32sys = AudioFormatHelpers.s32lsb 86 | AudioFormatHelpers.f32sys = AudioFormatHelpers.f32lsb 87 | } else { 88 | AudioFormatHelpers.s16sys = AudioFormatHelpers.s16msb 89 | AudioFormatHelpers.u16sys = AudioFormatHelpers.u16msb 90 | AudioFormatHelpers.s32sys = AudioFormatHelpers.s32msb 91 | AudioFormatHelpers.f32sys = AudioFormatHelpers.f32msb 92 | } 93 | 94 | module.exports = { AudioFormatHelpers } 95 | -------------------------------------------------------------------------------- /src/javascript/audio/index.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | const { AudioPlaybackInstance } = require('./audio-playback-instance') 5 | const { AudioRecordingInstance } = require('./audio-recording-instance') 6 | const { AudioFormatHelpers } = require('./format-helpers') 7 | 8 | 9 | Globals.audioDevices.playback = Bindings.audio_getDevices(false) 10 | Globals.audioDevices.recording = Bindings.audio_getDevices(true) 11 | 12 | 13 | const validEvents = [ 'deviceAdd', 'deviceRemove' ] 14 | 15 | const audio = new class extends EventsViaPoll { 16 | constructor () { super(validEvents) } 17 | 18 | get devices () { 19 | Globals.events.poll() 20 | return [ 21 | ...Globals.audioDevices.playback, 22 | ...Globals.audioDevices.recording, 23 | ] 24 | } 25 | 26 | openDevice (device, options = {}) { 27 | if (typeof device !== 'object') { throw Object.assign(new Error("device must be an object"), { device }) } 28 | if (typeof device.type !== 'string') { throw Object.assign(new Error("device.type must be a string"), { device }) } 29 | if (![ 'playback', 'recording' ].includes(device.type)) { throw Object.assign(new Error("invalid device.type"), { device }) } 30 | 31 | return device.type === 'recording' 32 | ? new AudioRecordingInstance(device, options) 33 | : new AudioPlaybackInstance(device, options) 34 | } 35 | 36 | bytesPerSample (format) { return AudioFormatHelpers[format].bytesPerSample } 37 | minSampleValue (format) { return AudioFormatHelpers[format].minSampleValue } 38 | maxSampleValue (format) { return AudioFormatHelpers[format].maxSampleValue } 39 | zeroSampleValue (format) { return AudioFormatHelpers[format].zeroSampleValue } 40 | 41 | readSample (format, buffer, offset) { 42 | return AudioFormatHelpers[format].reader.call(buffer, offset) 43 | } 44 | 45 | writeSample (format, buffer, value, offset) { 46 | return AudioFormatHelpers[format].writer.call(buffer, value, offset) 47 | } 48 | }() 49 | 50 | module.exports = { audio } 51 | -------------------------------------------------------------------------------- /src/javascript/audio/prevent-exit.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | 3 | let timeout 4 | 5 | const resetTimeout = () => { 6 | if (timeout) { 7 | clearTimeout(timeout) 8 | timeout = null 9 | } 10 | } 11 | 12 | process.on('beforeExit', (code) => { 13 | if (code !== 0) { return } 14 | 15 | let duration = 0 16 | 17 | for (const instance of Globals.audioInstances.values()) { 18 | if (instance.device.type === 'recording') { continue } 19 | 20 | const { queued, playing } = instance 21 | if (!queued || !playing) { continue } 22 | 23 | const { channels, frequency, buffered, bytesPerSample } = instance 24 | const bytesPerSecond = channels * frequency * bytesPerSample 25 | duration = Math.max(duration, (queued + buffered) / bytesPerSecond) 26 | } 27 | 28 | if (duration) { 29 | resetTimeout() 30 | timeout = setTimeout(() => { timeout = null }, duration * 1e3) 31 | } 32 | }) 33 | 34 | module.exports = { resetTimeout } 35 | -------------------------------------------------------------------------------- /src/javascript/bindings.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('../../dist/sdl.node') 3 | -------------------------------------------------------------------------------- /src/javascript/cleanup.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('./bindings') 2 | const Globals = require('./globals') 3 | 4 | process.on('exit', (code) => { 5 | if (code !== 0) { return } 6 | 7 | Globals.events.stopPolling() 8 | 9 | // Close all windows 10 | for (const window of Globals.windows.all.values()) { 11 | window.destroy() 12 | } 13 | 14 | // Close all audio instances 15 | for (const instance of Globals.audioInstances.values()) { 16 | instance.close() 17 | } 18 | 19 | // Close all joysticks 20 | for (const joystick of Globals.joystickInstances.all.values()) { 21 | joystick.close() 22 | } 23 | 24 | // Close all controllers 25 | for (const controller of Globals.controllerInstances.all.values()) { 26 | controller.close() 27 | } 28 | 29 | Bindings.global_cleanup() 30 | }) 31 | -------------------------------------------------------------------------------- /src/javascript/clipboard/index.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | 5 | const validEvents = [ 'update' ] 6 | 7 | const clipboard = new class extends EventsViaPoll { 8 | constructor () { super(validEvents) } 9 | 10 | get text () { 11 | Globals.events.poll() 12 | return Bindings.clipboard_getText() 13 | } 14 | 15 | setText (text) { 16 | if (typeof text !== 'string') { throw Object.assign(new Error("text must be a string"), { text }) } 17 | 18 | Bindings.clipboard_setText(text) 19 | } 20 | }() 21 | 22 | module.exports = { clipboard } 23 | -------------------------------------------------------------------------------- /src/javascript/controller/controller-instance.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | 5 | const validEvents = [ 6 | 'axisMotion', 7 | 'buttonDown', 8 | 'buttonUp', 9 | 'remap', 10 | 'close', 11 | ] 12 | 13 | class ControllerInstance extends EventsViaPoll { 14 | constructor (device) { 15 | super(validEvents) 16 | 17 | if (!Globals.controllerDevices.includes(device)) { throw Object.assign(new Error("invalid device"), { device }) } 18 | 19 | const result = Bindings.controller_open(device._index) 20 | 21 | this._firmwareVersion = result.firmwareVersion 22 | this._serialNumber = result.serialNumber 23 | this._hasLed = result.hasLed 24 | this._hasRumble = result.hasRumble 25 | this._hasRumbleTriggers = result.hasRumbleTriggers 26 | this._steamHandle = result.steamHandle 27 | this._axes = result.axes 28 | this._buttons = result.buttons 29 | 30 | this._device = device 31 | 32 | this._rumbleTimeout = null 33 | this._rumbleTriggersTimeout = null 34 | this._closed = false 35 | 36 | Globals.controllerInstances.all.add(this) 37 | let collection = Globals.controllerInstances.byId.get(this._device.id) 38 | if (!collection) { 39 | collection = new Set() 40 | Globals.controllerInstances.byId.set(this._device.id, collection) 41 | } 42 | collection.add(this) 43 | } 44 | 45 | get device () { return this._device } 46 | get firmwareVersion () { return this._firmwareVersion } 47 | get serialNumber () { return this._serialNumber } 48 | get steamHandle () { return this._steamHandle } 49 | 50 | get axes () { 51 | Globals.events.poll() 52 | return this._axes 53 | } 54 | 55 | get buttons () { 56 | Globals.events.poll() 57 | return this._buttons 58 | } 59 | 60 | get power () { return Bindings.joystick_getPower(this._device.id) } 61 | 62 | setPlayer (player) { 63 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 64 | 65 | if (!Number.isInteger(player)) { throw Object.assign(new Error("player must be an integer"), { player }) } 66 | if (player < 0) { throw Object.assign(new Error("invalid player"), { player }) } 67 | 68 | Bindings.joystick_setPlayer(this._device.id, player) 69 | } 70 | 71 | resetPlayer () { 72 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 73 | 74 | Bindings.joystick_setPlayer(this._device.id, -1) 75 | } 76 | 77 | get hasLed () { return this._hasLed } 78 | setLed (red, green, blue) { 79 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 80 | 81 | if (!Number.isFinite(red)) { throw Object.assign(new Error("red must be a number"), { red }) } 82 | if (red < 0 || red > 1) { throw Object.assign(new Error("red must be between 0 and 1"), { red }) } 83 | if (!Number.isFinite(green)) { throw Object.assign(new Error("green must be a number"), { green }) } 84 | if (green < 0 || green > 1) { throw Object.assign(new Error("green must be between 0 and 1"), { green }) } 85 | if (!Number.isFinite(blue)) { throw Object.assign(new Error("blue must be a number"), { blue }) } 86 | if (blue < 0 || blue > 1) { throw Object.assign(new Error("blue must be between 0 and 1"), { blue }) } 87 | 88 | Bindings.joystick_setLed(this._device.id, red, green, blue) 89 | } 90 | 91 | get hasRumble () { return this._hasRumble } 92 | rumble (lowFreqRumble = 1, highFreqRumble = 1, duration = 1e3) { 93 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 94 | 95 | if (!Number.isFinite(lowFreqRumble)) { throw Object.assign(new Error("lowFreqRumble must be a number"), { lowFreqRumble }) } 96 | if (lowFreqRumble < 0 || lowFreqRumble > 1) { throw Object.assign(new Error("lowFreqRumble must be between 0 and 1"), { lowFreqRumble }) } 97 | if (!Number.isFinite(highFreqRumble)) { throw Object.assign(new Error("highFreqRumble must be a number"), { highFreqRumble }) } 98 | if (highFreqRumble < 0 || highFreqRumble > 1) { throw Object.assign(new Error("highFreqRumble must be between 0 and 1"), { highFreqRumble }) } 99 | if (!Number.isFinite(duration)) { throw Object.assign(new Error("duration must be a number"), { duration }) } 100 | if (duration < 0) { throw Object.assign(new Error("invalid duration"), { duration }) } 101 | 102 | // Globals.events.poll() // Errors if it hasn't been called at least once 103 | Bindings.joystick_rumble(this._device.id, lowFreqRumble, highFreqRumble, duration) 104 | clearTimeout(this._rumbleTimeout) 105 | this._rumbleTimeout = setTimeout(() => { this.stopRumble() }, duration) 106 | } 107 | 108 | stopRumble () { 109 | clearTimeout(this._rumbleTimeout) 110 | this._rumbleTimeout = null 111 | Bindings.joystick_rumble(this._device.id, 0, 0, 0) 112 | Globals.events.poll() 113 | } 114 | 115 | get hasRumbleTriggers () { return this._hasRumbleTriggers } 116 | rumbleTriggers (leftRumble = 1, rightRumble = 1, duration = 1e3) { 117 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 118 | 119 | if (!Number.isFinite(leftRumble)) { throw Object.assign(new Error("leftRumble must be a number"), { leftRumble }) } 120 | if (leftRumble < 0 || leftRumble > 1) { throw Object.assign(new Error("leftRumble must be between 0 and 1"), { leftRumble }) } 121 | if (!Number.isFinite(rightRumble)) { throw Object.assign(new Error("rightRumble must be a number"), { rightRumble }) } 122 | if (rightRumble < 0 || rightRumble > 1) { throw Object.assign(new Error("rightRumble must be between 0 and 1"), { rightRumble }) } 123 | if (!Number.isFinite(duration)) { throw Object.assign(new Error("duration must be a number"), { duration }) } 124 | if (duration < 0) { throw Object.assign(new Error("invalid duration"), { duration }) } 125 | 126 | // Globals.events.poll() // Errors if it hasn't been called at least once 127 | Bindings.joystick_rumbleTriggers(this._device.id, leftRumble, rightRumble, duration) 128 | clearTimeout(this._rumbleTriggersTimeout) 129 | this._rumbleTriggersTimeout = setTimeout(() => { this.stopRumble() }, duration) 130 | } 131 | 132 | stopRumbleTriggers () { 133 | clearTimeout(this._rumbleTriggersTimeout) 134 | this._rumbleTriggersTimeout = null 135 | Bindings.joystick_rumbleTriggers(this._device.id, 0, 0, 0) 136 | Globals.events.poll() 137 | } 138 | 139 | get closed () { return this._closed } 140 | close () { 141 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 142 | 143 | this.emit('close') 144 | this.removeAllListeners() 145 | this._closed = true 146 | 147 | Globals.controllerInstances.all.delete(this) 148 | const collection = Globals.controllerInstances.byId.get(this._device.id) 149 | collection.delete(this) 150 | if (collection.size === 0) { 151 | Bindings.controller_close(this._device.id) 152 | Globals.controllerInstances.byId.delete(this._device.id) 153 | } 154 | } 155 | } 156 | 157 | module.exports = { ControllerInstance } 158 | -------------------------------------------------------------------------------- /src/javascript/controller/device.js: -------------------------------------------------------------------------------- 1 | 2 | const make = (device) => { 3 | const { 4 | isController, 5 | controllerMapping, 6 | controllerName, 7 | controllerType, 8 | ...rest 9 | } = device 10 | return { 11 | ...rest, 12 | mapping: controllerMapping, 13 | name: controllerName, 14 | type: controllerType, 15 | } 16 | } 17 | 18 | const compare = (a, b) => a._index - b._index 19 | 20 | const filter = (device) => device.isController 21 | 22 | module.exports = { 23 | make, 24 | compare, 25 | filter, 26 | } 27 | -------------------------------------------------------------------------------- /src/javascript/controller/index.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | const { ControllerInstance } = require('./controller-instance') 5 | const { reconcileJoystickAndControllerDevices } = require('../events/reconcile-joystick-and-controller-devices') 6 | 7 | const validEvents = [ 'deviceAdd', 'deviceRemove' ] 8 | 9 | const controller = new class extends EventsViaPoll { 10 | constructor () { super(validEvents) } 11 | 12 | get devices () { 13 | Globals.events.poll() 14 | return Globals.controllerDevices 15 | } 16 | 17 | openDevice (device) { return new ControllerInstance(device) } 18 | 19 | addMappings (mappings) { 20 | if (!Array.isArray(mappings)) { throw Object.assign(new Error("mappings must be an array"), { mappings }) } 21 | for (const mapping of mappings) { 22 | if (typeof mapping !== 'string') { throw Object.assign(new Error("mapping must be a string"), { mappings }) } 23 | } 24 | 25 | Bindings.controller_addMappings(mappings) 26 | Globals.events.poll() 27 | 28 | const devices = Bindings.joystick_getDevices() 29 | reconcileJoystickAndControllerDevices(devices) 30 | } 31 | }() 32 | 33 | module.exports = { controller } 34 | -------------------------------------------------------------------------------- /src/javascript/enums.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('./bindings') 2 | 3 | const enums = Bindings.enums_get() 4 | 5 | module.exports = enums 6 | -------------------------------------------------------------------------------- /src/javascript/events/events-via-poll.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const { EventEmitter } = require('events') 3 | 4 | let _ID = 0 5 | const activeEmitters = new Set() 6 | 7 | const commonEvents = [ 'newListener', 'removeListener', '*' ] 8 | 9 | class EventsViaPoll extends EventEmitter { 10 | constructor (validEvents) { 11 | super() 12 | 13 | this._validEvents = validEvents 14 | 15 | const id = _ID++ 16 | let count = 0 17 | 18 | // NOTE: this needs to be first, otherwise it will emit for 'newListener' 19 | this.on('removeListener', (type) => { 20 | if (!commonEvents.includes(type) && !validEvents.includes(type)) { 21 | throw Object.assign(new Error("invalid event"), { type }) 22 | } 23 | 24 | count-- 25 | if (count !== 0) { return } 26 | 27 | activeEmitters.delete(id) 28 | if (activeEmitters.size !== 0) { return } 29 | 30 | Globals.events.switchToPollingSlow() 31 | }) 32 | 33 | this.on('newListener', (type) => { 34 | if (!commonEvents.includes(type) && !validEvents.includes(type)) { 35 | throw Object.assign(new Error("invalid event"), { type }) 36 | } 37 | 38 | count++ 39 | if (count !== 1) { return } 40 | 41 | activeEmitters.add(id) 42 | if (activeEmitters.size !== 1) { return } 43 | 44 | Globals.events.switchToPollingFast() 45 | }) 46 | } 47 | 48 | emit (type, ...args) { 49 | const isCommon = commonEvents.includes(type) 50 | const isValid = this._validEvents.includes(type) 51 | if (!isCommon && !isValid) { 52 | throw Object.assign(new Error("invalid event"), { type }) 53 | } 54 | 55 | super.emit(type, ...args) 56 | 57 | if (!isCommon) { 58 | super.emit("*", type, ...args) 59 | } 60 | } 61 | } 62 | 63 | module.exports = { EventsViaPoll } 64 | -------------------------------------------------------------------------------- /src/javascript/events/reconcile-audio-devices.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const { reconcileDevices } = require('./reconcile') 3 | 4 | const { compare: compareAudioDevice } = require('../audio/device') 5 | 6 | const reconcileAudioDevices = (audioDevices, audioDeviceType) => { 7 | reconcileDevices( 8 | require('../audio').audio, 9 | Globals.audioDevices[audioDeviceType], 10 | audioDevices, 11 | compareAudioDevice, 12 | ) 13 | } 14 | 15 | module.exports = { reconcileAudioDevices } 16 | -------------------------------------------------------------------------------- /src/javascript/events/reconcile-displays.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const { reconcileDevices } = require('./reconcile') 3 | 4 | const { compare: compareDisplays } = require('../video/display') 5 | 6 | const reconcileDisplays = (displays) => { 7 | reconcileDevices( 8 | require('../video').video, 9 | Globals.displays, 10 | displays, 11 | compareDisplays, 12 | 'display', 13 | ) 14 | } 15 | 16 | module.exports = { reconcileDisplays } 17 | -------------------------------------------------------------------------------- /src/javascript/events/reconcile-joystick-and-controller-devices.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const { reconcileDevices } = require('./reconcile') 3 | 4 | const { 5 | make: makeJoystickDevice, 6 | compare: compareJoystickDevice, 7 | } = require('../joystick/device') 8 | const { 9 | make: makeControllerDevice, 10 | compare: compareControllerDevice, 11 | filter: filterControllerDevice, 12 | } = require('../controller/device') 13 | 14 | const reconcileJoystickAndControllerDevices = (devices) => { 15 | const joystickDevices = devices 16 | .map(makeJoystickDevice) 17 | 18 | const controllerDevices = devices 19 | .filter(filterControllerDevice) 20 | .map(makeControllerDevice) 21 | 22 | reconcileDevices( 23 | require('../joystick').joystick, 24 | Globals.joystickDevices, 25 | joystickDevices, 26 | compareJoystickDevice, 27 | ) 28 | 29 | reconcileDevices( 30 | require('../controller').controller, 31 | Globals.controllerDevices, 32 | controllerDevices, 33 | compareControllerDevice, 34 | ) 35 | } 36 | 37 | module.exports = { reconcileJoystickAndControllerDevices } 38 | -------------------------------------------------------------------------------- /src/javascript/events/reconcile.js: -------------------------------------------------------------------------------- 1 | 2 | const reconcileDevices = ( 3 | emitter, 4 | mainList, 5 | currList, 6 | compare, 7 | prefix = 'device', 8 | ) => { 9 | const addEventType = `${prefix}Add` 10 | const removeEventType = `${prefix}Remove` 11 | 12 | currList.sort(compare) 13 | 14 | let mainIndex = 0 15 | let currIndex = 0 16 | let mainDevice = mainList[mainIndex] 17 | let currDevice = currList[currIndex] 18 | while (mainIndex < mainList.length && currIndex < currList.length) { 19 | const cmp = compare(mainDevice, currDevice) 20 | if (cmp === 0) { 21 | Object.assign(mainList[mainIndex], currList[currIndex]) 22 | mainDevice = mainList[++mainIndex] 23 | currDevice = currList[++currIndex] 24 | } 25 | else if (cmp < 0) { 26 | mainList.splice(mainIndex, 1) 27 | const type = removeEventType 28 | const event = { type, device: mainDevice } 29 | emitter.emit(type, event) 30 | mainDevice = mainList[mainIndex] 31 | } 32 | else { 33 | mainList.splice(mainIndex, 0, currDevice) 34 | mainDevice = mainList[++mainIndex] 35 | const type = addEventType 36 | const event = { type, device: currDevice } 37 | emitter.emit(type, event) 38 | currDevice = currList[++currIndex] 39 | } 40 | } 41 | 42 | if (mainIndex < mainList.length) { 43 | while (mainIndex < mainList.length) { 44 | [ mainDevice ] = mainList.splice(mainIndex, 1) 45 | const type = removeEventType 46 | const event = { type, device: mainDevice } 47 | emitter.emit(type, event) 48 | } 49 | } 50 | else { 51 | while (currIndex < currList.length) { 52 | mainList.push(currDevice) 53 | const type = addEventType 54 | const event = { type, device: currDevice } 55 | emitter.emit(type, event) 56 | currDevice = currList[++currIndex] 57 | } 58 | } 59 | } 60 | 61 | module.exports = { reconcileDevices } 62 | -------------------------------------------------------------------------------- /src/javascript/globals.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | displays: [], 4 | windows: { 5 | all: new Map(), 6 | focused: null, 7 | hovered: null, 8 | }, 9 | joystickDevices: [], 10 | joystickInstances: { 11 | all: new Set(), 12 | byId: new Map(), 13 | }, 14 | controllerDevices: [], 15 | controllerInstances: { 16 | all: new Set(), 17 | byId: new Map(), 18 | }, 19 | sensorInstances: { 20 | all: new Set(), 21 | byId: new Map(), 22 | }, 23 | audioDevices: { 24 | playback: [], 25 | recording: [], 26 | }, 27 | audioInstances: new Map(), 28 | 29 | events: null, // Set later from events/index.js 30 | } 31 | -------------------------------------------------------------------------------- /src/javascript/helpers.js: -------------------------------------------------------------------------------- 1 | const { AudioFormatHelpers } = require('./audio/format-helpers') 2 | 3 | module.exports = { 4 | audio: { 5 | bytesPerSample (format) { return AudioFormatHelpers[format].bytesPerSample }, 6 | minSampleValue (format) { return AudioFormatHelpers[format].minSampleValue }, 7 | maxSampleValue (format) { return AudioFormatHelpers[format].maxSampleValue }, 8 | zeroSampleValue (format) { return AudioFormatHelpers[format].zeroSampleValue }, 9 | 10 | readSample (format, buffer, offset) { 11 | return AudioFormatHelpers[format].reader.call(buffer, offset) 12 | }, 13 | 14 | writeSample (format, buffer, value, offset) { 15 | return AudioFormatHelpers[format].writer.call(buffer, value, offset) 16 | }, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/javascript/index.js: -------------------------------------------------------------------------------- 1 | const { isMainThread } = require('worker_threads') 2 | if (!isMainThread) { throw new Error('@kmamal/sdl can only be used in the main thread') } 3 | 4 | const Bindings = require('./bindings') 5 | 6 | const info = Bindings.global_initialize() 7 | 8 | const { video } = require('./video') 9 | const { keyboard } = require('./keyboard') 10 | const { mouse } = require('./mouse') 11 | const { joystick } = require('./joystick') 12 | const { controller } = require('./controller') 13 | const { sensor } = require('./sensor') 14 | const { audio } = require('./audio') 15 | const { clipboard } = require('./clipboard') 16 | const { power } = require('./power') 17 | 18 | require('./events') 19 | require('./cleanup') 20 | 21 | module.exports = { 22 | info, 23 | video, 24 | keyboard, 25 | mouse, 26 | joystick, 27 | controller, 28 | sensor, 29 | audio, 30 | clipboard, 31 | power, 32 | } 33 | -------------------------------------------------------------------------------- /src/javascript/joystick/device.js: -------------------------------------------------------------------------------- 1 | 2 | const make = (device) => { 3 | const { 4 | isController, 5 | controllerMapping, 6 | controllerName, 7 | controllerType, 8 | ...rest 9 | } = device 10 | return rest 11 | } 12 | 13 | const compare = (a, b) => a._index - b._index 14 | 15 | module.exports = { 16 | make, 17 | compare, 18 | } 19 | -------------------------------------------------------------------------------- /src/javascript/joystick/index.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | const { JoystickInstance } = require('./joystick-instance') 5 | 6 | 7 | const { make: makeJoystickDevice } = require('./device') 8 | const { 9 | make: makeControllerDevice, 10 | filter: filterControllerDevice, 11 | } = require('../controller/device') 12 | 13 | const devices = Bindings.joystick_getDevices() 14 | Globals.joystickDevices = devices 15 | .map(makeJoystickDevice) 16 | Globals.controllerDevices = devices 17 | .filter(filterControllerDevice) 18 | .map(makeControllerDevice) 19 | 20 | 21 | const validEvents = [ 'deviceAdd', 'deviceRemove' ] 22 | 23 | const joystick = new class extends EventsViaPoll { 24 | constructor () { super(validEvents) } 25 | 26 | get devices () { 27 | Globals.events.poll() 28 | return Globals.joystickDevices 29 | } 30 | 31 | openDevice (device) { return new JoystickInstance(device) } 32 | }() 33 | 34 | module.exports = { joystick } 35 | -------------------------------------------------------------------------------- /src/javascript/joystick/joystick-instance.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | 5 | const validEvents = [ 6 | 'axisMotion', 7 | 'ballMotion', 8 | 'buttonDown', 9 | 'buttonUp', 10 | 'hatMotion', 11 | 'close', 12 | ] 13 | 14 | class JoystickInstance extends EventsViaPoll { 15 | constructor (device) { 16 | super(validEvents) 17 | 18 | if (!Globals.joystickDevices.includes(device)) { throw Object.assign(new Error("invalid device"), { device }) } 19 | 20 | const result = Bindings.joystick_open(device._index) 21 | 22 | this._firmwareVersion = result.firmwareVersion 23 | this._serialNumber = result.serialNumber 24 | this._hasLed = result.hasLed 25 | this._hasRumble = result.hasRumble 26 | this._hasRumbleTriggers = result.hasRumbleTriggers 27 | this._axes = result.axes 28 | this._balls = result.balls 29 | this._buttons = result.buttons 30 | this._hats = result.hats 31 | 32 | this._device = device 33 | 34 | this._rumbleTimeout = null 35 | this._rumbleTriggersTimeout = null 36 | this._closed = false 37 | 38 | Globals.joystickInstances.all.add(this) 39 | let collection = Globals.joystickInstances.byId.get(this._device.id) 40 | if (!collection) { 41 | collection = new Set() 42 | Globals.joystickInstances.byId.set(this._device.id, collection) 43 | } 44 | collection.add(this) 45 | } 46 | 47 | get device () { return this._device } 48 | get firmwareVersion () { return this._firmwareVersion } 49 | get serialNumber () { return this._serialNumber } 50 | 51 | get axes () { 52 | Globals.events.poll() 53 | return this._axes 54 | } 55 | 56 | get balls () { 57 | Globals.events.poll() 58 | return this._balls 59 | } 60 | 61 | get buttons () { 62 | Globals.events.poll() 63 | return this._buttons 64 | } 65 | 66 | get hats () { 67 | Globals.events.poll() 68 | return this._hats 69 | } 70 | 71 | get power () { return Bindings.joystick_getPower(this._device.id) } 72 | 73 | setPlayer (player) { 74 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 75 | 76 | if (!Number.isInteger(player)) { throw Object.assign(new Error("player must be an integer"), { player }) } 77 | if (player < 0) { throw Object.assign(new Error("invalid player"), { player }) } 78 | 79 | Bindings.joystick_setPlayer(this._device.id, player) 80 | } 81 | 82 | resetPlayer () { 83 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 84 | 85 | Bindings.joystick_setPlayer(this._device.id, -1) 86 | } 87 | 88 | get hasLed () { return this._hasLed } 89 | setLed (red, green, blue) { 90 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 91 | 92 | if (!Number.isFinite(red)) { throw Object.assign(new Error("red must be a number"), { red }) } 93 | if (red < 0 || red > 1) { throw Object.assign(new Error("red must be between 0 and 1"), { red }) } 94 | if (!Number.isFinite(green)) { throw Object.assign(new Error("green must be a number"), { green }) } 95 | if (green < 0 || green > 1) { throw Object.assign(new Error("green must be between 0 and 1"), { green }) } 96 | if (!Number.isFinite(blue)) { throw Object.assign(new Error("blue must be a number"), { blue }) } 97 | if (blue < 0 || blue > 1) { throw Object.assign(new Error("blue must be between 0 and 1"), { blue }) } 98 | 99 | Bindings.joystick_setLed(this._device.id, red, green, blue) 100 | } 101 | 102 | get hasRumble () { return this._hasRumble } 103 | rumble (lowFreqRumble = 1, highFreqRumble = 1, duration = 1e3) { 104 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 105 | 106 | if (!Number.isFinite(lowFreqRumble)) { throw Object.assign(new Error("lowFreqRumble must be a number"), { lowFreqRumble }) } 107 | if (lowFreqRumble < 0 || lowFreqRumble > 1) { throw Object.assign(new Error("lowFreqRumble must be between 0 and 1"), { lowFreqRumble }) } 108 | if (!Number.isFinite(highFreqRumble)) { throw Object.assign(new Error("highFreqRumble must be a number"), { highFreqRumble }) } 109 | if (highFreqRumble < 0 || highFreqRumble > 1) { throw Object.assign(new Error("highFreqRumble must be between 0 and 1"), { highFreqRumble }) } 110 | if (!Number.isInteger(duration)) { throw Object.assign(new Error("duration must be an integer"), { duration }) } 111 | if (duration < 0) { throw Object.assign(new Error("invalid duration"), { duration }) } 112 | 113 | // Globals.events.poll() // Errors if it hasn't been called at least once 114 | Bindings.joystick_rumble(this._device.id, lowFreqRumble, highFreqRumble, duration) 115 | clearTimeout(this._rumbleTimeout) 116 | this._rumbleTimeout = setTimeout(() => { this.stopRumble() }, duration) 117 | } 118 | 119 | stopRumble () { 120 | clearTimeout(this._rumbleTimeout) 121 | this._rumbleTimeout = null 122 | Bindings.joystick_rumble(this._device.id, 0, 0, 0) 123 | Globals.events.poll() 124 | } 125 | 126 | get hasRumbleTriggers () { return this._hasRumbleTriggers } 127 | rumbleTriggers (leftRumble = 1, rightRumble = 1, duration = 1e3) { 128 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 129 | 130 | if (!Number.isFinite(leftRumble)) { throw Object.assign(new Error("leftRumble must be a number"), { leftRumble }) } 131 | if (leftRumble < 0 || leftRumble > 1) { throw Object.assign(new Error("leftRumble must be between 0 and 1"), { leftRumble }) } 132 | if (!Number.isFinite(rightRumble)) { throw Object.assign(new Error("rightRumble must be a number"), { rightRumble }) } 133 | if (rightRumble < 0 || rightRumble > 1) { throw Object.assign(new Error("rightRumble must be between 0 and 1"), { rightRumble }) } 134 | if (!Number.isInteger(duration)) { throw Object.assign(new Error("duration must be an integer"), { duration }) } 135 | if (duration < 0) { throw Object.assign(new Error("invalid duration"), { duration }) } 136 | 137 | // Globals.events.poll() // Errors if it hasn't been called at least once 138 | Bindings.joystick_rumbleTriggers(this._device.id, leftRumble, rightRumble, duration) 139 | clearTimeout(this._rumbleTriggersTimeout) 140 | this._rumbleTriggersTimeout = setTimeout(() => { this.stopRumble() }, duration) 141 | } 142 | 143 | stopRumbleTriggers () { 144 | clearTimeout(this._rumbleTriggersTimeout) 145 | this._rumbleTriggersTimeout = null 146 | Bindings.joystick_rumbleTriggers(this._device.id, 0, 0, 0) 147 | Globals.events.poll() 148 | } 149 | 150 | get closed () { return this._closed } 151 | close () { 152 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 153 | 154 | this.emit('close') 155 | this.removeAllListeners() 156 | this._closed = true 157 | 158 | Globals.joystickInstances.all.delete(this) 159 | const collection = Globals.joystickInstances.byId.get(this._device.id) 160 | collection.delete(this) 161 | if (collection.size === 0) { 162 | Bindings.joystick_close(this._device.id) 163 | Globals.joystickInstances.byId.delete(this._device.id) 164 | } 165 | } 166 | } 167 | 168 | module.exports = { JoystickInstance } 169 | -------------------------------------------------------------------------------- /src/javascript/keyboard/index.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const Enums = require('../enums') 4 | const { mapping, reverseMapping } = require('./key-mapping') 5 | 6 | const keyboard = { 7 | get SCANCODE () { return Enums.scancodes }, 8 | 9 | getKey (scancode) { 10 | if (!Number.isInteger(scancode)) { throw Object.assign(new Error("scancode must be an integer"), { scancode }) } 11 | if (scancode < 0 || scancode >= 512) { throw Object.assign(new Error("invalid scancode"), { scancode }) } 12 | 13 | const _key = Bindings.keyboard_getKey(scancode) 14 | return mapping[_key] ?? (_key?.length === 1 ? _key : null) 15 | }, 16 | 17 | getScancode (key) { 18 | if (typeof key !== 'string') { throw Object.assign(new Error("key must be a string"), { key }) } 19 | 20 | const _key = reverseMapping[key] ?? (key.length === 1 ? key : null) 21 | if (_key === null) { throw Object.assign(new Error("invalid key"), { key }) } 22 | 23 | return Bindings.keyboard_getScancode(_key) 24 | }, 25 | 26 | getState () { 27 | Globals.events.poll() 28 | return Bindings.keyboard_getState() 29 | }, 30 | } 31 | 32 | module.exports = { keyboard } 33 | -------------------------------------------------------------------------------- /src/javascript/keyboard/key-mapping.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('../bindings') 2 | 3 | const mapping = { 4 | 'A': 'a', 5 | 'AC Back': 'back', 6 | 'AC Bookmarks': 'bookmarks', 7 | 'AC Forward': 'forward', 8 | 'AC Home': 'home', 9 | 'AC Refresh': 'refresh', 10 | 'AC Search': 'search', 11 | 'AC Stop': 'stop', 12 | 'Again': 'again', 13 | 'AltErase': 'altErase', 14 | 'App1': 'app1', 15 | 'App2': 'app2', 16 | 'Application': 'application', 17 | 'AudioFastForward': 'audioFastForward', 18 | 'AudioMute': 'audioMute', 19 | 'AudioNext': 'audioNext', 20 | 'AudioPlay': 'audioPlay', 21 | 'AudioPrev': 'audioPrev', 22 | 'AudioRewind': 'audioRewind', 23 | 'AudioStop': 'audioStop', 24 | 'B': 'b', 25 | 'Backspace': 'backspace', 26 | 'BrightnessDown': 'brightnessDown', 27 | 'BrightnessUp': 'brightnessUp', 28 | 'C': 'c', 29 | 'Calculator': 'calculator', 30 | 'Cancel': 'cancel', 31 | 'CapsLock': 'capsLock', 32 | 'Clear / Again': 'clear/again', 33 | 'Clear': 'clear', 34 | 'Computer': 'computer', 35 | 'Copy': 'copy', 36 | 'CrSel': 'crSel', 37 | 'CurrencySubUnit': 'currencySubUnit', 38 | 'CurrencyUnit': 'currencyUnit', 39 | 'Cut': 'cut', 40 | 'D': 'd', 41 | 'DecimalSeparator': 'decimalSeparator', 42 | 'Delete': 'delete', 43 | 'DisplaySwitch': 'displaySwitch', 44 | 'Down': 'down', 45 | 'E': 'e', 46 | 'Eject': 'eject', 47 | 'End': 'end', 48 | 'Escape': 'escape', 49 | 'Execute': 'execute', 50 | 'ExSel': 'exSel', 51 | 'F': 'f', 52 | 'F1': 'f1', 53 | 'F10': 'f10', 54 | 'F11': 'f11', 55 | 'F12': 'f12', 56 | 'F13': 'f13', 57 | 'F14': 'f14', 58 | 'F15': 'f15', 59 | 'F16': 'f16', 60 | 'F17': 'f17', 61 | 'F18': 'f18', 62 | 'F19': 'f19', 63 | 'F2': 'f2', 64 | 'F20': 'f20', 65 | 'F21': 'f21', 66 | 'F22': 'f22', 67 | 'F23': 'f23', 68 | 'F24': 'f24', 69 | 'F3': 'f3', 70 | 'F4': 'f4', 71 | 'F5': 'f5', 72 | 'F6': 'f6', 73 | 'F7': 'f7', 74 | 'F8': 'f8', 75 | 'F9': 'f9', 76 | 'Find': 'find', 77 | 'G': 'g', 78 | 'H': 'h', 79 | 'Help': 'help', 80 | 'Home': 'home', 81 | 'I': 'i', 82 | 'Insert': 'insert', 83 | 'J': 'j', 84 | 'K': 'k', 85 | 'KBDIllumDown': 'illumDown', 86 | 'KBDIllumToggle': 'illumToggle', 87 | 'KBDIllumUp': 'illumUp', 88 | 'Keypad -': '-', 89 | 'Keypad ,': ',', 90 | 'Keypad :': ':', 91 | 'Keypad !': '!', 92 | 'Keypad .': '.', 93 | 'Keypad (': '(', 94 | 'Keypad )': ')', 95 | 'Keypad {': '{', 96 | 'Keypad }': '}', 97 | 'Keypad @': '@', 98 | 'Keypad *': '*', 99 | 'Keypad /': '/', 100 | 'Keypad &': '&', 101 | 'Keypad &&': '&&', 102 | 'Keypad #': '#', 103 | 'Keypad %': '%', 104 | 'Keypad ^': '^', 105 | 'Keypad +': '+', 106 | 'Keypad +/-': '+/-', 107 | 'Keypad <': '<', 108 | 'Keypad = (AS400)': '=', 109 | 'Keypad =': '=', 110 | 'Keypad >': '>', 111 | 'Keypad |': '|', 112 | 'Keypad ||': '||', 113 | 'Keypad 0': '0', 114 | 'Keypad 00': '00', 115 | 'Keypad 000': '000', 116 | 'Keypad 1': '1', 117 | 'Keypad 2': '2', 118 | 'Keypad 3': '3', 119 | 'Keypad 4': '4', 120 | 'Keypad 5': '5', 121 | 'Keypad 6': '6', 122 | 'Keypad 7': '7', 123 | 'Keypad 8': '8', 124 | 'Keypad 9': '9', 125 | 'Keypad A': 'a', 126 | 'Keypad B': 'b', 127 | 'Keypad Backspace': 'backspace', 128 | 'Keypad Binary': 'binary', 129 | 'Keypad C': 'c', 130 | 'Keypad Clear': 'clear', 131 | 'Keypad ClearEntry': 'clearEntry', 132 | 'Keypad D': 'd', 133 | 'Keypad Decimal': 'decimal', 134 | 'Keypad E': 'e', 135 | 'Keypad Enter': 'enter', 136 | 'Keypad F': 'f', 137 | 'Keypad Hexadecimal': 'hexadecimal', 138 | 'Keypad MemAdd': 'memAdd', 139 | 'Keypad MemClear': 'memClear', 140 | 'Keypad MemDivide': 'memDivide', 141 | 'Keypad MemMultiply': 'memMultiply', 142 | 'Keypad MemRecall': 'memRecall', 143 | 'Keypad MemStore': 'memStore', 144 | 'Keypad MemSubtract': 'memSubtract', 145 | 'Keypad Octal': 'octal', 146 | 'Keypad Space': ' ', 147 | 'Keypad Tab': 'tab', 148 | 'Keypad XOR': 'xor', 149 | 'L': 'l', 150 | 'Left Alt': 'alt', 151 | 'Left Ctrl': 'ctrl', 152 | 'Left GUI': 'gui', 153 | 'Left Shift': 'shift', 154 | 'Left': 'left', 155 | 'M': 'm', 156 | 'Mail': 'mail', 157 | 'MediaSelect': 'mediaSelect', 158 | 'Menu': 'menu', 159 | 'ModeSwitch': 'modeSwitch', 160 | 'Mute': 'mute', 161 | 'N': 'n', 162 | 'Numlock': 'numlock', 163 | 'O': 'o', 164 | 'Oper': 'oper', 165 | 'Out': 'out', 166 | 'P': 'p', 167 | 'PageDown': 'pageDown', 168 | 'PageUp': 'pageUp', 169 | 'Paste': 'paste', 170 | 'Pause': 'pause', 171 | 'Power': 'power', 172 | 'PrintScreen': 'printScreen', 173 | 'Prior': 'prior', 174 | 'Q': 'q', 175 | 'R': 'r', 176 | 'Return': 'return', 177 | 'Right Alt': 'alt', 178 | 'Right Ctrl': 'ctrl', 179 | 'Right GUI': 'gUI', 180 | 'Right Shift': 'shift', 181 | 'Right': 'right', 182 | 'S': 's', 183 | 'ScrollLock': 'scrollLock', 184 | 'Select': 'select', 185 | 'Separator': 'separator', 186 | 'Sleep': 'sleep', 187 | 'Space': 'space', 188 | 'Stop': 'stop', 189 | 'SysReq': 'sysReq', 190 | 'T': 't', 191 | 'Tab': 'tab', 192 | 'ThousandsSeparator': 'thousandsSeparator', 193 | 'U': 'u', 194 | 'Undo': 'undo', 195 | 'Up': 'up', 196 | 'V': 'v', 197 | 'VolumeDown': 'volumeDown', 198 | 'VolumeUp': 'volumeUp', 199 | 'W': 'w', 200 | 'WWW': 'www', 201 | 'X': 'x', 202 | 'Y': 'y', 203 | 'Z': 'z', 204 | } 205 | 206 | const reverseMapping = {} 207 | 208 | for (const [ key, value ] of Object.entries(mapping)) { 209 | maybeSkip: { 210 | const existing = reverseMapping[value] 211 | if (existing === undefined) { break maybeSkip } 212 | 213 | const keyScancode = Bindings.keyboard_getScancode(key) 214 | if (keyScancode === null) { continue } 215 | 216 | const existingScancode = Bindings.keyboard_getScancode(existing) 217 | if (existingScancode === null) { break maybeSkip } 218 | 219 | if (existingScancode < keyScancode) { continue } 220 | } 221 | 222 | reverseMapping[value] = key 223 | } 224 | 225 | module.exports = { 226 | mapping, 227 | reverseMapping, 228 | } 229 | -------------------------------------------------------------------------------- /src/javascript/mouse/index.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('../bindings') 2 | const Enums = require('../enums') 3 | 4 | const mouse = { 5 | get BUTTON () { return Enums.mouseButtons }, 6 | 7 | getButton (button) { 8 | if (!Number.isInteger(button)) { throw Object.assign(new Error("button must be an integer"), { button }) } 9 | if (button < 0 || button >= 32) { throw Object.assign(new Error("invalid button"), { button }) } 10 | 11 | return Bindings.mouse_getButton(button) 12 | }, 13 | 14 | get position () { return Bindings.mouse_getPosition() }, 15 | 16 | setPosition (x, y) { 17 | if (!Number.isInteger(x)) { throw Object.assign(new Error("x must be an integer"), { x }) } 18 | if (!Number.isInteger(y)) { throw Object.assign(new Error("y must be an integer"), { y }) } 19 | 20 | Bindings.mouse_setPosition(x, y) 21 | }, 22 | 23 | setCursor (cursor) { 24 | if (typeof cursor !== 'string') { throw Object.assign(new Error("cursor must be a string"), { cursor }) } 25 | 26 | const _cursor = Enums.cursors[cursor] 27 | if (_cursor === undefined) { throw Object.assign(new Error("invalid cursor"), { cursor }) } 28 | 29 | Bindings.mouse_setCursor(_cursor) 30 | }, 31 | 32 | resetCursor () { Bindings.mouse_resetCursor() }, 33 | 34 | setCursorImage (width, height, stride, format, buffer, x, y) { 35 | if (!Number.isInteger(width)) { throw Object.assign(new Error("width must be an integer"), { width }) } 36 | if (width <= 0) { throw Object.assign(new Error("invalid width"), { width }) } 37 | if (!Number.isInteger(height)) { throw Object.assign(new Error("height must be an integer"), { height }) } 38 | if (height <= 0) { throw Object.assign(new Error("invalid height"), { height }) } 39 | if (!Number.isInteger(stride)) { throw Object.assign(new Error("stride must be an integer"), { stride }) } 40 | if (stride < width) { throw Object.assign(new Error("invalid stride"), { stride, width }) } 41 | if (typeof format !== 'string') { throw Object.assign(new Error("format must be a string"), { format }) } 42 | if (!(buffer instanceof Buffer)) { throw Object.assign(new Error("buffer must be a Buffer"), { buffer }) } 43 | if (buffer.length < stride * height) { throw Object.assign(new Error("buffer is smaller than expected"), { buffer, stride, height }) } 44 | if (!Number.isInteger(x)) { throw Object.assign(new Error("x must be an integer"), { x }) } 45 | if (x < 0 || x >= width) { throw Object.assign(new Error("invalid x"), { x }) } 46 | if (!Number.isInteger(y)) { throw Object.assign(new Error("y must be an integer"), { y }) } 47 | if (y < 0 || y >= height) { throw Object.assign(new Error("invalid y"), { y }) } 48 | 49 | const _format = Enums.pixelFormat[format] 50 | if (_format === undefined) { throw Object.assign(new Error("invalid format"), { format }) } 51 | 52 | Bindings.mouse_setCursorImage(width, height, stride, _format, buffer, x, y) 53 | }, 54 | 55 | showCursor (show = true) { 56 | if (typeof show !== 'boolean') { throw Object.assign(new Error("show must be a boolean"), { show }) } 57 | 58 | Bindings.mouse_showCursor(show) 59 | }, 60 | 61 | hideCursor () { mouse.showCursor(false) }, 62 | 63 | redrawCursor () { Bindings.mouse_redrawCursor() }, 64 | 65 | capture (capture = true) { 66 | if (typeof capture !== 'boolean') { throw Object.assign(new Error("capture must be a boolean"), { capture }) } 67 | 68 | Bindings.mouse_capture(capture) 69 | }, 70 | 71 | uncapture () { mouse.capture(false) }, 72 | } 73 | 74 | module.exports = { mouse } 75 | -------------------------------------------------------------------------------- /src/javascript/power/index.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('../bindings') 2 | 3 | const power = { 4 | get info () { return Bindings.power_getInfo() }, 5 | } 6 | 7 | module.exports = { power } 8 | -------------------------------------------------------------------------------- /src/javascript/sensor/index.js: -------------------------------------------------------------------------------- 1 | const Bindings = require('../bindings') 2 | const { SensorInstance } = require('./sensor-instance') 3 | 4 | 5 | const sensor = { 6 | STANDARD_GRAVITY: 9.80665, 7 | 8 | get devices () { 9 | const devices = Bindings.sensor_getDevices() 10 | for (const sensorDevice of devices) { 11 | const { type } = sensorDevice 12 | sensorDevice.type 13 | = type.startsWith('accelerometer') ? 'accelerometer' 14 | : type.startsWith('gyroscope') ? 'gyroscope' 15 | : 'unknown' 16 | sensorDevice.side 17 | = type.endsWith('Left') ? 'left' 18 | : type.endsWith('Right') ? 'right' 19 | : null 20 | } 21 | return devices 22 | }, 23 | 24 | openDevice (device) { return new SensorInstance(device) }, 25 | } 26 | 27 | module.exports = { sensor } 28 | -------------------------------------------------------------------------------- /src/javascript/sensor/sensor-instance.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | 5 | const validEvents = [ 6 | 'update', 7 | 'close', 8 | ] 9 | 10 | class SensorInstance extends EventsViaPoll { 11 | constructor (device) { 12 | super(validEvents) 13 | 14 | if (!Globals.sensorDevices.includes(device)) { throw Object.assign(new Error("invalid device"), { device }) } 15 | 16 | Bindings.sensor_open(device._index) 17 | 18 | this._device = device 19 | 20 | this._closed = false 21 | 22 | Globals.sensorInstances.all.add(this) 23 | let collection = Globals.sensorInstances.byId.get(this._device.id) 24 | if (!collection) { 25 | collection = new Set() 26 | Globals.sensorInstances.byId.set(this._device.id, collection) 27 | } 28 | collection.add(this) 29 | } 30 | 31 | get device () { return this._device } 32 | 33 | get data () { return Bindings.sensor_getData(this._device.id) } 34 | 35 | get closed () { return this._closed } 36 | close () { 37 | if (this._closed) { throw Object.assign(new Error("instance is closed"), { id: this._device.id }) } 38 | 39 | this.emit('close') 40 | this.removeAllListeners() 41 | this._closed = true 42 | 43 | Globals.sensorInstances.all.delete(this) 44 | const collection = Globals.sensorInstances.byId.get(this._device.id) 45 | collection.delete(this) 46 | if (collection.size === 0) { 47 | Bindings.sensor_close(this._device.id) 48 | Globals.sensorInstances.byId.delete(this._device.id) 49 | } 50 | } 51 | } 52 | 53 | module.exports = { SensorInstance } 54 | -------------------------------------------------------------------------------- /src/javascript/video/display.js: -------------------------------------------------------------------------------- 1 | 2 | const compare = (a, b) => a._index - b._index 3 | 4 | module.exports = { compare } 5 | -------------------------------------------------------------------------------- /src/javascript/video/index.js: -------------------------------------------------------------------------------- 1 | const Globals = require('../globals') 2 | const Bindings = require('../bindings') 3 | const { EventsViaPoll } = require('../events/events-via-poll') 4 | const { Window } = require('./window') 5 | 6 | 7 | Globals.displays = Bindings.video_getDisplays() 8 | 9 | 10 | const validEvents = [ 'displayAdd', 'displayRemove', 'displayOrient' ] 11 | 12 | const video = new class extends EventsViaPoll { 13 | constructor () { super(validEvents) } 14 | 15 | get displays () { 16 | Globals.events.poll() 17 | return Globals.displays 18 | } 19 | 20 | get windows () { return [ ...Globals.windows.all.values() ] } 21 | get focused () { return Globals.windows.focused } 22 | get hovered () { return Globals.windows.hovered } 23 | 24 | createWindow (options) { return new Window(options) } 25 | }() 26 | 27 | module.exports = { video } 28 | -------------------------------------------------------------------------------- /src/native/audio.cpp: -------------------------------------------------------------------------------- 1 | #include "audio.h" 2 | #include "global.h" 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | std::map audio::device_types; 9 | 10 | 11 | Napi::Array 12 | audio::_getDevices(Napi::Env &env, bool is_capture) 13 | { 14 | int num_devices = SDL_GetNumAudioDevices(is_capture ? 1 : 0); 15 | if (num_devices == -1) { 16 | // SDL_GetNumAudioDevices can return -1 even if there is no error 17 | const char *error = SDL_GetError(); 18 | if (error != global::no_error) { 19 | std::ostringstream message; 20 | message << "SDL_GetNumAudioDevices(" << is_capture << ") error: " << error; 21 | SDL_ClearError(); 22 | throw Napi::Error::New(env, message.str()); 23 | } 24 | return Napi::Array::New(env); 25 | } 26 | 27 | Napi::Array devices = Napi::Array::New(env, num_devices); 28 | 29 | std::string device_type = audio::device_types[is_capture]; 30 | 31 | for (int i = 0; i < num_devices; i++) { 32 | const char *name = SDL_GetAudioDeviceName(i, is_capture); 33 | if (name == nullptr) { 34 | std::ostringstream message; 35 | message << "SDL_GetAudioDeviceName(" << i << ", " << is_capture << ") error: " << SDL_GetError(); 36 | SDL_ClearError(); 37 | throw Napi::Error::New(env, message.str()); 38 | } 39 | 40 | Napi::Object device = Napi::Object::New(env); 41 | device.Set("name", name); 42 | device.Set("type", device_type); 43 | 44 | devices.Set(i, device); 45 | } 46 | 47 | return devices; 48 | } 49 | 50 | 51 | Napi::Value 52 | audio::getDevices(const Napi::CallbackInfo &info) 53 | { 54 | Napi::Env env = info.Env(); 55 | 56 | bool is_capture = info[0].As().Value(); 57 | 58 | return audio::_getDevices(env, is_capture); 59 | } 60 | 61 | #include 62 | 63 | Napi::Value 64 | audio::open (const Napi::CallbackInfo &info) 65 | { 66 | Napi::Env env = info.Env(); 67 | 68 | std::string name; 69 | bool has_name = !info[0].IsNull(); 70 | if (has_name) { name = info[0].As().Utf8Value(); } 71 | bool is_capture = info[1].As().Value(); 72 | int freq = info[2].As().Int32Value(); 73 | int format = info[3].As().Int32Value(); 74 | int channels = info[4].As().Int32Value(); 75 | int samples = info[5].As().Int32Value(); 76 | 77 | SDL_AudioSpec desired; 78 | SDL_memset(&desired, 0, sizeof(desired)); 79 | desired.freq = freq; 80 | desired.format = format; 81 | desired.channels = channels; 82 | desired.samples = samples; 83 | 84 | SDL_AudioDeviceID audio_id = SDL_OpenAudioDevice(has_name ? name.c_str() : nullptr, is_capture ? 1 : 0, &desired, nullptr, 0); 85 | if (audio_id == 0) { 86 | std::ostringstream message; 87 | message << "SDL_OpenAudioDevice() error: " << SDL_GetError(); 88 | SDL_ClearError(); 89 | throw Napi::Error::New(env, message.str()); 90 | } 91 | 92 | return Napi::Number::New(env, audio_id); 93 | } 94 | 95 | Napi::Value 96 | audio::play (const Napi::CallbackInfo &info) 97 | { 98 | Napi::Env env = info.Env(); 99 | 100 | int audio_id = info[0].As().Int32Value(); 101 | bool should_play = info[1].As().Value(); 102 | 103 | SDL_PauseAudioDevice(audio_id, should_play ? 0 : 1); 104 | 105 | return env.Undefined(); 106 | } 107 | 108 | Napi::Value 109 | audio::getQueueSize (const Napi::CallbackInfo &info) 110 | { 111 | Napi::Env env = info.Env(); 112 | 113 | int audio_id = info[0].As().Int32Value(); 114 | 115 | int size = SDL_GetQueuedAudioSize(audio_id); 116 | 117 | return Napi::Number::New(env, size); 118 | } 119 | 120 | Napi::Value 121 | audio::clearQueue (const Napi::CallbackInfo &info) 122 | { 123 | Napi::Env env = info.Env(); 124 | 125 | int audio_id = info[0].As().Int32Value(); 126 | 127 | SDL_ClearQueuedAudio(audio_id); 128 | 129 | return env.Undefined(); 130 | } 131 | 132 | Napi::Value 133 | audio::enqueue (const Napi::CallbackInfo &info) 134 | { 135 | Napi::Env env = info.Env(); 136 | 137 | int audio_id = info[0].As().Int32Value(); 138 | void *src = info[1].As>().Data(); 139 | int size = info[2].As().Int32Value(); 140 | 141 | if (SDL_QueueAudio(audio_id, src, size) < 0) { 142 | std::ostringstream message; 143 | message << "SDL_QueueAudio() error: " << SDL_GetError(); 144 | SDL_ClearError(); 145 | throw Napi::Error::New(env, message.str()); 146 | } 147 | 148 | return env.Undefined(); 149 | } 150 | 151 | Napi::Value 152 | audio::dequeue (const Napi::CallbackInfo &info) 153 | { 154 | Napi::Env env = info.Env(); 155 | 156 | int audio_id = info[0].As().Int32Value(); 157 | void *dst = info[1].As>().Data(); 158 | int size = info[2].As().Int32Value(); 159 | 160 | int num = SDL_DequeueAudio(audio_id, dst, size); 161 | const char *error = SDL_GetError(); 162 | if (error != global::no_error) { 163 | std::ostringstream message; 164 | message << "SDL_DequeueAudio(" << audio_id << ") error: " << error; 165 | SDL_ClearError(); 166 | throw Napi::Error::New(env, message.str()); 167 | } 168 | 169 | return Napi::Number::New(env, num); 170 | } 171 | 172 | Napi::Value 173 | audio::close (const Napi::CallbackInfo &info) 174 | { 175 | Napi::Env env = info.Env(); 176 | 177 | int audio_id = info[0].As().Int32Value(); 178 | 179 | SDL_PauseAudioDevice(audio_id, 1); 180 | SDL_ClearQueuedAudio(audio_id); 181 | SDL_CloseAudioDevice(audio_id); 182 | 183 | return env.Undefined(); 184 | } 185 | -------------------------------------------------------------------------------- /src/native/audio.h: -------------------------------------------------------------------------------- 1 | #ifndef _AUDIO_H_ 2 | #define _AUDIO_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace audio { 9 | 10 | extern std::map device_types; 11 | 12 | Napi::Array _getDevices(Napi::Env &env, bool is_capture); 13 | 14 | Napi::Value getDevices(const Napi::CallbackInfo &info); 15 | Napi::Value open(const Napi::CallbackInfo &info); 16 | Napi::Value play(const Napi::CallbackInfo &info); 17 | Napi::Value getQueueSize(const Napi::CallbackInfo &info); 18 | Napi::Value enqueue(const Napi::CallbackInfo &info); 19 | Napi::Value dequeue(const Napi::CallbackInfo &info); 20 | Napi::Value clearQueue(const Napi::CallbackInfo &info); 21 | Napi::Value close(const Napi::CallbackInfo &info); 22 | 23 | }; // namespace audio 24 | 25 | #endif // _AUDIO_H_ 26 | -------------------------------------------------------------------------------- /src/native/clipboard.cpp: -------------------------------------------------------------------------------- 1 | #include "clipboard.h" 2 | #include 3 | #include 4 | #include 5 | 6 | Napi::Value 7 | clipboard::getText (const Napi::CallbackInfo &info) 8 | { 9 | Napi::Env env = info.Env(); 10 | 11 | if (SDL_HasClipboardText() == SDL_FALSE) { 12 | return Napi::String::New(env, ""); 13 | } 14 | 15 | char *text = SDL_GetClipboardText(); 16 | if (text[0] == '\0') { 17 | SDL_free(text); 18 | std::ostringstream message; 19 | message << "SDL_GetClipboardText() error: " << SDL_GetError(); 20 | SDL_ClearError(); 21 | throw Napi::Error::New(env, message.str()); 22 | } 23 | 24 | Napi::String result = Napi::String::New(env, text); 25 | SDL_free(text); 26 | 27 | return result; 28 | } 29 | 30 | Napi::Value 31 | clipboard::setText (const Napi::CallbackInfo &info) 32 | { 33 | Napi::Env env = info.Env(); 34 | 35 | std::string text = info[0].As().Utf8Value(); 36 | 37 | if (SDL_SetClipboardText(text.c_str()) < 0) { 38 | std::ostringstream message; 39 | message << "SDL_SetClipboardText() error: " << SDL_GetError(); 40 | SDL_ClearError(); 41 | throw Napi::Error::New(env, message.str()); 42 | } 43 | 44 | return env.Undefined(); 45 | } 46 | -------------------------------------------------------------------------------- /src/native/clipboard.h: -------------------------------------------------------------------------------- 1 | #ifndef _CLIPBOARD_H_ 2 | #define _CLIPBOARD_H_ 3 | 4 | #include 5 | 6 | namespace clipboard { 7 | 8 | Napi::Value getText(const Napi::CallbackInfo &info); 9 | Napi::Value setText(const Napi::CallbackInfo &info); 10 | 11 | }; // namespace clipboard 12 | 13 | #endif // _CLIPBOARD_H_ 14 | -------------------------------------------------------------------------------- /src/native/cocoa-global.h: -------------------------------------------------------------------------------- 1 | #ifndef _COCOA_GLOBAL_H_ 2 | #define _COCOA_GLOBAL_H_ 3 | 4 | 5 | extern "C" void reenableInertialScrolling(); 6 | 7 | 8 | #endif // _COCOA_GLOBAL_H_ 9 | -------------------------------------------------------------------------------- /src/native/cocoa-global.mm: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "cocoa-global.h" 4 | 5 | 6 | extern "C" void reenableInertialScrolling() { 7 | [[NSUserDefaults standardUserDefaults] setBool: YES forKey: @"AppleMomentumScrollSupported"]; 8 | } 9 | -------------------------------------------------------------------------------- /src/native/cocoa-window.h: -------------------------------------------------------------------------------- 1 | #ifndef _COCOA_WINDOW_H_ 2 | #define _COCOA_WINDOW_H_ 3 | 4 | 5 | #ifndef __OBJC__ 6 | class NSView; 7 | class CALayer; 8 | #endif 9 | 10 | 11 | extern "C" NSView *getCocoaWindowHandle(NSWindow *); 12 | 13 | extern "C" CALayer *getCocoaGlView(NSWindow *); 14 | 15 | extern "C" CALayer *getCocoaGpuView(NSWindow *); 16 | 17 | 18 | #endif // _COCOA_WINDOW_H_ 19 | -------------------------------------------------------------------------------- /src/native/cocoa-window.mm: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "cocoa-window.h" 5 | 6 | 7 | extern "C" NSView *getCocoaWindowHandle(NSWindow *window) { 8 | @autoreleasepool { 9 | return [window contentView]; 10 | } 11 | } 12 | 13 | extern "C" CALayer *getCocoaGlView(NSWindow *window) { 14 | @autoreleasepool { 15 | NSView *view = [window contentView]; 16 | [view setWantsLayer:YES]; 17 | [[view layer] setContentsScale:[window backingScaleFactor]]; 18 | return [view layer]; 19 | } 20 | } 21 | 22 | extern "C" CALayer *getCocoaGpuView(NSWindow *window) { 23 | @autoreleasepool { 24 | NSView *view = [window contentView]; 25 | [view setWantsLayer:YES]; 26 | [view setLayer:[CAMetalLayer layer]]; 27 | [[view layer] setContentsScale:[window backingScaleFactor]]; 28 | return [view layer]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/native/controller.cpp: -------------------------------------------------------------------------------- 1 | #include "controller.h" 2 | #include "joystick.h" 3 | #include "global.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | std::map controller::types; 11 | std::map controller::axes; 12 | std::map controller::buttons; 13 | 14 | 15 | double 16 | controller::mapAxis (SDL_GameController *controller, SDL_GameControllerAxis axis) { 17 | return mapAxisValue(controller, axis, SDL_GameControllerGetAxis(controller, axis)); 18 | } 19 | 20 | double 21 | controller::mapAxisValue (SDL_GameController *controller, SDL_GameControllerAxis axis, int value) { 22 | SDL_GameControllerButtonBind bind = SDL_GameControllerGetBindForAxis(controller, axis); 23 | if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS) { 24 | SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller); 25 | return joystick::mapAxisValue(joystick, bind.value.axis, value); 26 | } 27 | 28 | double range = value < 0 ? -SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX; 29 | return value / range; 30 | } 31 | 32 | void 33 | controller::getState (Napi::Env &env, SDL_GameController *controller, Napi::Object dst) 34 | { 35 | Napi::Object axes = Napi::Object::New(env); 36 | axes.Set("leftStickX", controller::mapAxis(controller, SDL_CONTROLLER_AXIS_LEFTX)); 37 | axes.Set("leftStickY", controller::mapAxis(controller, SDL_CONTROLLER_AXIS_LEFTY)); 38 | axes.Set("rightStickX", controller::mapAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX)); 39 | axes.Set("rightStickY", controller::mapAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY)); 40 | axes.Set("leftTrigger", controller::mapAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT)); 41 | axes.Set("rightTrigger", controller::mapAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT)); 42 | 43 | const char *error; 44 | 45 | error = SDL_GetError(); 46 | if (error != global::no_error) { 47 | std::ostringstream message; 48 | message << "SDL_GameControllerGetAxis() error: " << error; 49 | SDL_ClearError(); 50 | throw Napi::Error::New(env, message.str()); 51 | } 52 | 53 | Napi::Object buttons = Napi::Object::New(env); 54 | buttons.Set("dpadLeft", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT)); 55 | buttons.Set("dpadRight", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)); 56 | buttons.Set("dpadUp", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP)); 57 | buttons.Set("dpadDown", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN)); 58 | buttons.Set("a", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A)); 59 | buttons.Set("b", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B)); 60 | buttons.Set("x", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X)); 61 | buttons.Set("y", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_Y)); 62 | buttons.Set("guide", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_GUIDE)); 63 | buttons.Set("back", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK)); 64 | buttons.Set("start", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START)); 65 | buttons.Set("leftStick", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSTICK)); 66 | buttons.Set("rightStick", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSTICK)); 67 | buttons.Set("leftShoulder", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER)); 68 | buttons.Set("rightShoulder", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)); 69 | buttons.Set("paddle1", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_PADDLE1)); 70 | buttons.Set("paddle2", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_PADDLE2)); 71 | buttons.Set("paddle3", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_PADDLE3)); 72 | buttons.Set("paddle4", !!SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_PADDLE4)); 73 | 74 | error = SDL_GetError(); 75 | if (error != global::no_error) { 76 | std::ostringstream message; 77 | message << "SDL_GameControllerGetButton() error: " << error; 78 | SDL_ClearError(); 79 | throw Napi::Error::New(env, message.str()); 80 | } 81 | 82 | dst.Set("axes", axes); 83 | dst.Set("buttons", buttons); 84 | } 85 | 86 | 87 | Napi::Value 88 | controller::addMappings (const Napi::CallbackInfo &info) 89 | { 90 | Napi::Env env = info.Env(); 91 | 92 | Napi::Array mappings = info[0].As(); 93 | 94 | for (int i = 0; i < (int) mappings.Length(); i++) { 95 | std::string mapping = mappings.Get(i).As().Utf8Value(); 96 | if (SDL_GameControllerAddMapping(mapping.c_str()) == -1) { 97 | std::ostringstream message; 98 | message << "SDL_GameControllerAddMapping(" << mapping << ") error: " << SDL_GetError(); 99 | SDL_ClearError(); 100 | throw Napi::Error::New(env, message.str()); 101 | } 102 | 103 | // SDL_GameControllerAddMapping produces errors even though it succeeds 104 | const char *error = SDL_GetError(); 105 | if (error != global::no_error) { fprintf(stderr, "SDL silent error: %s\n", error); } 106 | SDL_ClearError(); 107 | } 108 | 109 | return env.Undefined(); 110 | } 111 | 112 | Napi::Value 113 | controller::open (const Napi::CallbackInfo &info) 114 | { 115 | Napi::Env env = info.Env(); 116 | 117 | int index = info[0].As().Int32Value(); 118 | 119 | SDL_GameController *controller = SDL_GameControllerOpen(index); 120 | if (controller == nullptr) { 121 | std::ostringstream message; 122 | message << "SDL_GameControllerOpen(" << index << ") error: " << SDL_GetError(); 123 | SDL_ClearError(); 124 | throw Napi::Error::New(env, message.str()); 125 | } 126 | 127 | // SDL_GameControllerOpen produces errors even though it succeeds 128 | const char *error = SDL_GetError(); 129 | if (error != global::no_error) { fprintf(stderr, "SDL silent error: %s\n", error); } 130 | SDL_ClearError(); 131 | 132 | int _firmware_version = SDL_GameControllerGetFirmwareVersion(controller); 133 | Napi::Value firmware_version = _firmware_version != 0 134 | ? Napi::Number::New(env, _firmware_version) 135 | : env.Null() ; 136 | 137 | const char *_serial_number = SDL_GameControllerGetSerial(controller); 138 | Napi::Value serial_number = _serial_number != 0 139 | ? Napi::String::New(env, _serial_number) 140 | : env.Null(); 141 | 142 | bool has_led = SDL_GameControllerHasLED(controller); 143 | bool has_rumble = SDL_GameControllerHasRumble(controller); 144 | bool has_rumble_triggers = SDL_GameControllerHasRumbleTriggers(controller); 145 | 146 | Uint64 _steam_handle = SDL_GameControllerGetSteamHandle(controller); 147 | Napi::Value steam_handle = _steam_handle != 0 148 | ? Napi::Buffer::Copy(env, &_steam_handle, 1) 149 | : env.Null(); 150 | 151 | Napi::Object result = Napi::Object::New(env); 152 | result.Set("firmwareVersion", firmware_version); 153 | result.Set("serialNumber", serial_number); 154 | result.Set("hasLed", has_led); 155 | result.Set("hasRumble", has_rumble); 156 | result.Set("hasRumbleTriggers", has_rumble_triggers); 157 | result.Set("steamHandle", steam_handle); 158 | 159 | controller::getState(env, controller, result); 160 | 161 | return result; 162 | } 163 | 164 | Napi::Value 165 | controller::close (const Napi::CallbackInfo &info) 166 | { 167 | Napi::Env env = info.Env(); 168 | 169 | int controller_id = info[0].As().Int32Value(); 170 | 171 | SDL_GameController *controller = SDL_GameControllerFromInstanceID(controller_id); 172 | if (controller == nullptr) { 173 | std::ostringstream message; 174 | message << "SDL_GameControllerFromInstanceID(" << controller_id << ") error: " << SDL_GetError(); 175 | SDL_ClearError(); 176 | throw Napi::Error::New(env, message.str()); 177 | } 178 | 179 | SDL_GameControllerClose(controller); 180 | 181 | return env.Undefined(); 182 | } 183 | -------------------------------------------------------------------------------- /src/native/controller.h: -------------------------------------------------------------------------------- 1 | #ifndef _CONTROLLER_H_ 2 | #define _CONTROLLER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace controller { 10 | 11 | extern std::map types; 12 | extern std::map axes; 13 | extern std::map buttons; 14 | 15 | double mapAxis (SDL_GameController *controller, SDL_GameControllerAxis axis); 16 | double mapAxisValue (SDL_GameController *controller, SDL_GameControllerAxis axis, int value); 17 | void getState (Napi::Env &env, SDL_GameController *controller, Napi::Object dst); 18 | 19 | Napi::Value addMappings(const Napi::CallbackInfo &info); 20 | Napi::Value open(const Napi::CallbackInfo &info); 21 | Napi::Value close(const Napi::CallbackInfo &info); 22 | 23 | }; // namespace controller 24 | 25 | #endif // _CONTROLLER_H_ 26 | -------------------------------------------------------------------------------- /src/native/enums.h: -------------------------------------------------------------------------------- 1 | #ifndef _ENUMS_H_ 2 | #define _ENUMS_H_ 3 | 4 | #include 5 | 6 | namespace enums { 7 | 8 | Napi::Value get(const Napi::CallbackInfo &info); 9 | 10 | }; // namespace enums 11 | 12 | #endif // _ENUMS_H_ 13 | -------------------------------------------------------------------------------- /src/native/events.h: -------------------------------------------------------------------------------- 1 | #ifndef _EVENTS_H_ 2 | #define _EVENTS_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace events { 9 | 10 | namespace families { 11 | extern std::string APP; 12 | extern std::string DISPLAY; 13 | extern std::string WINDOW; 14 | extern std::string DROP; 15 | extern std::string KEYBOARD; 16 | extern std::string TEXT; 17 | extern std::string MOUSE; 18 | extern std::string JOYSTICK_DEVICE; 19 | extern std::string JOYSTICK; 20 | extern std::string CONTROLLER; 21 | extern std::string SENSOR; 22 | extern std::string AUDIO_DEVICE; 23 | extern std::string CLIPBOARD; 24 | }; 25 | 26 | namespace types { 27 | extern std::string QUIT; 28 | extern std::string DISPLAY_ADD; 29 | extern std::string DISPLAY_REMOVE; 30 | extern std::string DISPLAY_ORIENT; 31 | extern std::string DISPLAY_MOVE; 32 | extern std::string DISPLAY_CHANGE; 33 | extern std::string SHOW; 34 | extern std::string HIDE; 35 | extern std::string EXPOSE; 36 | extern std::string MOVE; 37 | extern std::string RESIZE; 38 | extern std::string MINIMIZE; 39 | extern std::string MAXIMIZE; 40 | extern std::string RESTORE; 41 | extern std::string FOCUS; 42 | extern std::string BLUR; 43 | extern std::string HOVER; 44 | extern std::string LEAVE; 45 | extern std::string KEY_DOWN; 46 | extern std::string KEY_UP; 47 | extern std::string TEXT_INPUT; 48 | extern std::string MOUSE_MOVE; 49 | extern std::string MOUSE_BUTTON_DOWN; 50 | extern std::string MOUSE_BUTTON_UP; 51 | extern std::string MOUSE_WHEEL; 52 | extern std::string DROP_BEGIN; 53 | extern std::string DROP_COMPLETE; 54 | extern std::string DROP_FILE; 55 | extern std::string DROP_TEXT; 56 | extern std::string CLOSE; 57 | extern std::string DEVICE_ADD; 58 | extern std::string DEVICE_REMOVE; 59 | extern std::string AXIS_MOTION; 60 | extern std::string BUTTON_DOWN; 61 | extern std::string BUTTON_UP; 62 | extern std::string BALL_MOTION; 63 | extern std::string HAT_MOTION; 64 | extern std::string REMAP; 65 | extern std::string UPDATE; 66 | }; 67 | 68 | void dispatchEvent(const SDL_Event &event); 69 | 70 | Napi::Value poll(const Napi::CallbackInfo &info); 71 | 72 | }; // namespace events 73 | 74 | #endif // _EVENTS_H_ 75 | -------------------------------------------------------------------------------- /src/native/global.h: -------------------------------------------------------------------------------- 1 | #ifndef _GLOBAL_H_ 2 | #define _GLOBAL_H_ 3 | 4 | #include 5 | 6 | namespace global { 7 | 8 | extern const char *no_error; 9 | 10 | Napi::Value initialize(const Napi::CallbackInfo &info); 11 | Napi::Value cleanup(const Napi::CallbackInfo &info); 12 | 13 | }; // namespace global 14 | 15 | #endif // _GLOBAL_H_ 16 | -------------------------------------------------------------------------------- /src/native/joystick.h: -------------------------------------------------------------------------------- 1 | #ifndef _JOYSTICK_H_ 2 | #define _JOYSTICK_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace joystick { 10 | 11 | extern std::map types; 12 | extern std::map hat_positions; 13 | extern std::map power_levels; 14 | 15 | double mapAxis (SDL_Joystick *joystick, int axis); 16 | double mapAxisValue (SDL_Joystick *joystick, int axis, int value); 17 | Napi::Array _getDevices (Napi::Env &env); 18 | 19 | Napi::Value getDevices(const Napi::CallbackInfo &info); 20 | Napi::Value open(const Napi::CallbackInfo &info); 21 | Napi::Value getPower(const Napi::CallbackInfo &info); 22 | Napi::Value setLed(const Napi::CallbackInfo &info); 23 | Napi::Value setPlayer(const Napi::CallbackInfo &info); 24 | Napi::Value rumble(const Napi::CallbackInfo &info); 25 | Napi::Value rumbleTriggers(const Napi::CallbackInfo &info); 26 | Napi::Value close(const Napi::CallbackInfo &info); 27 | 28 | }; // namespace joystick 29 | 30 | #endif // _JOYSTICK_H_ 31 | -------------------------------------------------------------------------------- /src/native/keyboard.cpp: -------------------------------------------------------------------------------- 1 | #include "keyboard.h" 2 | #include 3 | #include 4 | #include 5 | 6 | int keyboard::num_keys; 7 | const Uint8 *keyboard::keys; 8 | 9 | 10 | Napi::Value 11 | keyboard::getKey(const Napi::CallbackInfo &info) 12 | { 13 | Napi::Env env = info.Env(); 14 | 15 | int scancode = info[0].As().Int32Value(); 16 | 17 | int keycode = SDL_GetKeyFromScancode((SDL_Scancode) scancode); 18 | const char *keyname = SDL_GetKeyName(keycode); 19 | 20 | return Napi::String::New(env, keyname); 21 | } 22 | 23 | Napi::Value 24 | keyboard::getScancode(const Napi::CallbackInfo &info) 25 | { 26 | Napi::Env env = info.Env(); 27 | 28 | std::string keyname = info[0].As().Utf8Value(); 29 | 30 | SDL_Keycode keycode = SDL_GetKeyFromName(keyname.c_str()); 31 | if (keycode == SDLK_UNKNOWN) { 32 | SDL_ClearError(); 33 | return env.Null(); 34 | } 35 | 36 | int scancode = SDL_GetScancodeFromKey(keycode); 37 | if (scancode == 0) { 38 | SDL_ClearError(); 39 | return env.Null(); 40 | } 41 | 42 | return Napi::Number::New(env, scancode); 43 | } 44 | 45 | Napi::Value 46 | keyboard::getState(const Napi::CallbackInfo &info) 47 | { 48 | Napi::Env env = info.Env(); 49 | 50 | Napi::Array result = Napi::Array::New(env, keyboard::num_keys); 51 | 52 | for (int i = 0 ; i < keyboard::num_keys; i++) { 53 | result.Set(i, !!keyboard::keys[i]); 54 | } 55 | 56 | return result; 57 | } 58 | -------------------------------------------------------------------------------- /src/native/keyboard.h: -------------------------------------------------------------------------------- 1 | #ifndef _KEYBOARD_H_ 2 | #define _KEYBOARD_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace keyboard { 8 | 9 | extern int num_keys; 10 | extern const Uint8 *keys; 11 | 12 | Napi::Value getKey(const Napi::CallbackInfo &info); 13 | Napi::Value getScancode(const Napi::CallbackInfo &info); 14 | Napi::Value getState(const Napi::CallbackInfo &info); 15 | 16 | }; // namespace keyboard 17 | 18 | #endif // _KEYBOARD_H_ 19 | -------------------------------------------------------------------------------- /src/native/module.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "enums.h" 4 | #include "global.h" 5 | #include "events.h" 6 | #include "video.h" 7 | #include "window.h" 8 | #include "keyboard.h" 9 | #include "mouse.h" 10 | #include "joystick.h" 11 | #include "controller.h" 12 | #include "sensor.h" 13 | #include "audio.h" 14 | #include "clipboard.h" 15 | #include "power.h" 16 | 17 | 18 | Napi::Object 19 | init (Napi::Env env, Napi::Object exports) 20 | { 21 | exports.Set("enums_get", Napi::Function::New(env)); 22 | 23 | exports.Set("global_initialize", Napi::Function::New(env)); 24 | exports.Set("global_cleanup", Napi::Function::New(env)); 25 | 26 | exports.Set("events_poll", Napi::Function::New(env)); 27 | 28 | exports.Set("video_getDisplays", Napi::Function::New(env)); 29 | 30 | exports.Set("window_create", Napi::Function::New(env)); 31 | exports.Set("window_setTitle", Napi::Function::New(env)); 32 | exports.Set("window_setPosition", Napi::Function::New(env)); 33 | exports.Set("window_setSize", Napi::Function::New(env)); 34 | exports.Set("window_setFullscreen", Napi::Function::New(env)); 35 | exports.Set("window_setResizable", Napi::Function::New(env)); 36 | exports.Set("window_setBorderless", Napi::Function::New(env)); 37 | exports.Set("window_setAcceleratedAndVsync", Napi::Function::New(env)); 38 | exports.Set("window_focus", Napi::Function::New(env)); 39 | exports.Set("window_show", Napi::Function::New(env)); 40 | exports.Set("window_hide", Napi::Function::New(env)); 41 | exports.Set("window_maximize", Napi::Function::New(env)); 42 | exports.Set("window_minimize", Napi::Function::New(env)); 43 | exports.Set("window_restore", Napi::Function::New(env)); 44 | exports.Set("window_render", Napi::Function::New(env)); 45 | exports.Set("window_setIcon", Napi::Function::New(env)); 46 | exports.Set("window_flash", Napi::Function::New(env)); 47 | exports.Set("window_destroy", Napi::Function::New(env)); 48 | 49 | exports.Set("keyboard_getKey", Napi::Function::New(env)); 50 | exports.Set("keyboard_getScancode", Napi::Function::New(env)); 51 | exports.Set("keyboard_getState", Napi::Function::New(env)); 52 | 53 | exports.Set("mouse_getButton", Napi::Function::New(env)); 54 | exports.Set("mouse_getPosition", Napi::Function::New(env)); 55 | exports.Set("mouse_setPosition", Napi::Function::New(env)); 56 | exports.Set("mouse_capture", Napi::Function::New(env)); 57 | exports.Set("mouse_setCursor", Napi::Function::New(env)); 58 | exports.Set("mouse_resetCursor", Napi::Function::New(env)); 59 | exports.Set("mouse_setCursorImage", Napi::Function::New(env)); 60 | exports.Set("mouse_showCursor", Napi::Function::New(env)); 61 | exports.Set("mouse_redrawCursor", Napi::Function::New(env)); 62 | 63 | exports.Set("joystick_getDevices", Napi::Function::New(env)); 64 | exports.Set("joystick_open", Napi::Function::New(env)); 65 | exports.Set("joystick_getPower", Napi::Function::New(env)); 66 | exports.Set("joystick_setLed", Napi::Function::New(env)); 67 | exports.Set("joystick_setPlayer", Napi::Function::New(env)); 68 | exports.Set("joystick_rumble", Napi::Function::New(env)); 69 | exports.Set("joystick_rumbleTriggers", Napi::Function::New(env)); 70 | exports.Set("joystick_close", Napi::Function::New(env)); 71 | 72 | exports.Set("controller_addMappings", Napi::Function::New(env)); 73 | exports.Set("controller_open", Napi::Function::New(env)); 74 | exports.Set("controller_close", Napi::Function::New(env)); 75 | 76 | exports.Set("sensor_getDevices", Napi::Function::New(env)); 77 | exports.Set("sensor_open", Napi::Function::New(env)); 78 | exports.Set("sensor_getData", Napi::Function::New(env)); 79 | exports.Set("sensor_close", Napi::Function::New(env)); 80 | 81 | exports.Set("audio_getDevices", Napi::Function::New(env)); 82 | exports.Set("audio_open", Napi::Function::New(env)); 83 | exports.Set("audio_play", Napi::Function::New(env)); 84 | exports.Set("audio_getQueueSize", Napi::Function::New(env)); 85 | exports.Set("audio_enqueue", Napi::Function::New(env)); 86 | exports.Set("audio_dequeue", Napi::Function::New(env)); 87 | exports.Set("audio_clearQueue", Napi::Function::New(env)); 88 | exports.Set("audio_close", Napi::Function::New(env)); 89 | 90 | exports.Set("clipboard_getText", Napi::Function::New(env)); 91 | exports.Set("clipboard_setText", Napi::Function::New(env)); 92 | 93 | exports.Set("power_getInfo", Napi::Function::New(env)); 94 | 95 | return exports; 96 | } 97 | 98 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, init) 99 | -------------------------------------------------------------------------------- /src/native/mouse.cpp: -------------------------------------------------------------------------------- 1 | #include "mouse.h" 2 | #include 3 | #include 4 | #include 5 | 6 | static SDL_Cursor *allocated_cursor = nullptr; 7 | 8 | 9 | Napi::Value 10 | mouse::getButton (const Napi::CallbackInfo &info) 11 | { 12 | Napi::Env env = info.Env(); 13 | 14 | int button = info[0].As().Int32Value(); 15 | 16 | int buttons = SDL_GetMouseState(nullptr, nullptr); 17 | bool state = buttons & SDL_BUTTON(button); 18 | 19 | return Napi::Boolean::New(env, state); 20 | } 21 | 22 | Napi::Value 23 | mouse::getPosition (const Napi::CallbackInfo &info) 24 | { 25 | Napi::Env env = info.Env(); 26 | 27 | int x, y; 28 | SDL_GetGlobalMouseState(&x, &y); 29 | 30 | Napi::Object result = Napi::Object::New(env); 31 | result.Set("x", x); 32 | result.Set("y", y); 33 | 34 | return result; 35 | } 36 | 37 | Napi::Value 38 | mouse::setPosition (const Napi::CallbackInfo &info) 39 | { 40 | Napi::Env env = info.Env(); 41 | 42 | int x = info[0].As().Int32Value(); 43 | int y = info[1].As().Int32Value(); 44 | 45 | if (SDL_WarpMouseGlobal(x, y) < 0) { 46 | std::ostringstream message; 47 | message << "SDL_WarpMouseGlobal(" << x << ", " << y << ") error: " << SDL_GetError(); 48 | SDL_ClearError(); 49 | throw Napi::Error::New(env, message.str()); 50 | } 51 | 52 | return env.Undefined(); 53 | } 54 | 55 | Napi::Value 56 | mouse::setCursor (const Napi::CallbackInfo &info) 57 | { 58 | Napi::Env env = info.Env(); 59 | 60 | int cursor_id = info[0].As().Int32Value(); 61 | 62 | if (allocated_cursor != nullptr) { 63 | SDL_FreeCursor(allocated_cursor); 64 | allocated_cursor = nullptr; 65 | } 66 | 67 | SDL_Cursor *cursor = SDL_CreateSystemCursor((SDL_SystemCursor) cursor_id); 68 | if (cursor == nullptr) { 69 | std::ostringstream message; 70 | message << "SDL_CreateSystemCursor(" << cursor_id << ") error: " << SDL_GetError(); 71 | SDL_ClearError(); 72 | throw Napi::Error::New(env, message.str()); 73 | } 74 | 75 | SDL_SetCursor(cursor); 76 | 77 | return env.Undefined(); 78 | } 79 | 80 | Napi::Value 81 | mouse::resetCursor(const Napi::CallbackInfo &info) 82 | { 83 | Napi::Env env = info.Env(); 84 | 85 | if (allocated_cursor != nullptr) { 86 | SDL_FreeCursor(allocated_cursor); 87 | allocated_cursor = nullptr; 88 | } 89 | 90 | SDL_Cursor *cursor = SDL_GetDefaultCursor(); 91 | if (cursor == nullptr) { 92 | std::ostringstream message; 93 | message << "SDL_GetDefaultCursor() error: " << SDL_GetError(); 94 | SDL_ClearError(); 95 | throw Napi::Error::New(env, message.str()); 96 | } 97 | 98 | SDL_SetCursor(cursor); 99 | 100 | return env.Undefined(); 101 | } 102 | 103 | Napi::Value 104 | mouse::setCursorImage (const Napi::CallbackInfo &info) 105 | { 106 | Napi::Env env = info.Env(); 107 | 108 | int w = info[0].As().Int32Value(); 109 | int h = info[1].As().Int32Value(); 110 | int stride = info[2].As().Int32Value(); 111 | unsigned int format = info[3].As().Int32Value(); 112 | void *pixels = info[4].As>().Data(); 113 | int x = info[5].As().Int32Value(); 114 | int y = info[6].As().Int32Value(); 115 | 116 | SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormatFrom(pixels, w, h, SDL_BITSPERPIXEL(format), stride, format); 117 | if (surface == nullptr) { 118 | std::ostringstream message; 119 | message << "SDL_CreateRGBSurfaceWithFormatFrom(" << w << ", " << h << ", " << format << ") error: " << SDL_GetError(); 120 | SDL_ClearError(); 121 | throw Napi::Error::New(env, message.str()); 122 | } 123 | 124 | if (allocated_cursor != nullptr) { 125 | SDL_FreeCursor(allocated_cursor); 126 | allocated_cursor = nullptr; 127 | } 128 | 129 | allocated_cursor = SDL_CreateColorCursor(surface, x, y); 130 | if (allocated_cursor == nullptr) { 131 | SDL_FreeSurface(surface); 132 | 133 | std::ostringstream message; 134 | message << "SDL_CreateColorCursor(" << x << ", " << y << ") error: " << SDL_GetError(); 135 | SDL_ClearError(); 136 | throw Napi::Error::New(env, message.str()); 137 | } 138 | 139 | SDL_SetCursor(allocated_cursor); 140 | 141 | SDL_FreeSurface(surface); 142 | return env.Undefined(); 143 | } 144 | 145 | Napi::Value 146 | mouse::showCursor (const Napi::CallbackInfo &info) 147 | { 148 | Napi::Env env = info.Env(); 149 | 150 | bool should_show = info[0].As().Value(); 151 | 152 | if (SDL_ShowCursor(should_show ? SDL_ENABLE : SDL_DISABLE) < 0) { 153 | std::ostringstream message; 154 | message << "SDL_ShowCursor(" << should_show << ") error: " << SDL_GetError(); 155 | SDL_ClearError(); 156 | throw Napi::Error::New(env, message.str()); 157 | } 158 | 159 | return env.Undefined(); 160 | } 161 | 162 | Napi::Value 163 | mouse::redrawCursor (const Napi::CallbackInfo &info) 164 | { 165 | Napi::Env env = info.Env(); 166 | 167 | SDL_SetCursor(nullptr); 168 | 169 | return env.Undefined(); 170 | } 171 | 172 | Napi::Value 173 | mouse::capture (const Napi::CallbackInfo &info) 174 | { 175 | Napi::Env env = info.Env(); 176 | 177 | bool should_capture = info[0].As().Value(); 178 | 179 | if (SDL_CaptureMouse(should_capture ? SDL_TRUE : SDL_FALSE) == -1) { 180 | std::ostringstream message; 181 | message << "SDL_CaptureMouse(" << should_capture << ") error: " << SDL_GetError(); 182 | SDL_ClearError(); 183 | throw Napi::Error::New(env, message.str()); 184 | } 185 | 186 | return env.Undefined(); 187 | } 188 | -------------------------------------------------------------------------------- /src/native/mouse.h: -------------------------------------------------------------------------------- 1 | #ifndef _MOUSE_H_ 2 | #define _MOUSE_H_ 3 | 4 | #include 5 | 6 | namespace mouse { 7 | 8 | Napi::Value getButton(const Napi::CallbackInfo &info); 9 | Napi::Value getPosition(const Napi::CallbackInfo &info); 10 | Napi::Value setPosition(const Napi::CallbackInfo &info); 11 | Napi::Value getCursor(const Napi::CallbackInfo &info); 12 | Napi::Value setCursor(const Napi::CallbackInfo &info); 13 | Napi::Value resetCursor(const Napi::CallbackInfo &info); 14 | Napi::Value setCursorImage(const Napi::CallbackInfo &info); 15 | Napi::Value showCursor(const Napi::CallbackInfo &info); 16 | Napi::Value redrawCursor(const Napi::CallbackInfo &info); 17 | Napi::Value capture(const Napi::CallbackInfo &info); 18 | 19 | }; // namespace mouse 20 | 21 | #endif // _MOUSE_H_ 22 | -------------------------------------------------------------------------------- /src/native/power.cpp: -------------------------------------------------------------------------------- 1 | #include "power.h" 2 | #include 3 | #include 4 | 5 | 6 | std::map power::states; 7 | 8 | 9 | Napi::Value 10 | power::getInfo (const Napi::CallbackInfo &info) 11 | { 12 | Napi::Env env = info.Env(); 13 | 14 | int _seconds, _percent; 15 | SDL_PowerState _state = SDL_GetPowerInfo(&_seconds, &_percent); 16 | 17 | Napi::Value state = _state != SDL_POWERSTATE_UNKNOWN 18 | ? Napi::String::New(env, power::states[_state]) 19 | : env.Null(); 20 | Napi::Value seconds = _seconds != -1 21 | ? Napi::Number::New(env, _seconds) 22 | : env.Null(); 23 | Napi::Value percent = _percent != -1 24 | ? Napi::Number::New(env, _percent) 25 | : env.Null(); 26 | 27 | Napi::Object result = Napi::Object::New(env); 28 | result.Set("state", state); 29 | result.Set("seconds", seconds); 30 | result.Set("percent", percent); 31 | 32 | return result; 33 | } 34 | -------------------------------------------------------------------------------- /src/native/power.h: -------------------------------------------------------------------------------- 1 | #ifndef _POWER_H_ 2 | #define _POWER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace power { 10 | 11 | extern std::map states; 12 | 13 | Napi::Value getInfo(const Napi::CallbackInfo &info); 14 | 15 | }; // namespace power 16 | 17 | #endif // _POWER_H_ 18 | -------------------------------------------------------------------------------- /src/native/sensor.cpp: -------------------------------------------------------------------------------- 1 | #include "sensor.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | std::map sensor::types; 9 | std::map sensor::sides; 10 | 11 | 12 | Napi::Value 13 | sensor::getDevices (const Napi::CallbackInfo &info) 14 | { 15 | Napi::Env env = info.Env(); 16 | 17 | int num_devices = SDL_NumSensors(); 18 | if (num_devices < 0) { 19 | std::ostringstream message; 20 | message << "SDL_NumSensors() error: " << SDL_GetError(); 21 | SDL_ClearError(); 22 | throw Napi::Error::New(env, message.str()); 23 | } 24 | 25 | Napi::Array devices = Napi::Array::New(env, num_devices); 26 | 27 | for (int i = 0; i < num_devices; i++) { 28 | int id = SDL_SensorGetDeviceInstanceID(i); 29 | if (id == -1) { 30 | std::ostringstream message; 31 | message << "SDL_SensorGetDeviceInstanceID(" << i << ") error: " << SDL_GetError(); 32 | SDL_ClearError(); 33 | throw Napi::Error::New(env, message.str()); 34 | } 35 | 36 | // This function can only error if the index is invalid. 37 | SDL_SensorType _type = SDL_SensorGetDeviceType(i); 38 | Napi::Value type = _type != SDL_SENSOR_UNKNOWN 39 | ? Napi::String::New(env, sensor::types[_type]) 40 | : env.Null(); 41 | Napi::Value side = ( 42 | _type != SDL_SENSOR_UNKNOWN && 43 | _type != SDL_SENSOR_ACCEL && 44 | _type != SDL_SENSOR_GYRO 45 | ) 46 | ? Napi::String::New(env, sensor::sides[_type]) 47 | : env.Null(); 48 | 49 | // This function can only error if the index is invalid. 50 | const char *name = SDL_SensorGetDeviceName(i); 51 | 52 | Napi::Object device = Napi::Object::New(env); 53 | device.Set("_index", i); 54 | device.Set("id", id); 55 | device.Set("name", name); 56 | device.Set("type", type); 57 | device.Set("side", side); 58 | 59 | devices.Set(i, device); 60 | } 61 | 62 | return devices; 63 | } 64 | 65 | Napi::Value 66 | sensor::open (const Napi::CallbackInfo &info) 67 | { 68 | Napi::Env env = info.Env(); 69 | 70 | int index = info[0].As().Int32Value(); 71 | 72 | SDL_Sensor *sensor = SDL_SensorOpen(index); 73 | if (sensor == nullptr) { 74 | std::ostringstream message; 75 | message << "SDL_SensorOpen(" << index << ") error: " << SDL_GetError(); 76 | SDL_ClearError(); 77 | throw Napi::Error::New(env, message.str()); 78 | } 79 | 80 | return env.Undefined(); 81 | } 82 | 83 | Napi::Value 84 | sensor::getData (const Napi::CallbackInfo &info) 85 | { 86 | Napi::Env env = info.Env(); 87 | 88 | int sensor_id = info[0].As().Int32Value(); 89 | 90 | SDL_Sensor *sensor = SDL_SensorFromInstanceID(sensor_id); 91 | if (sensor == nullptr) { 92 | std::ostringstream message; 93 | message << "SDL_SensorFromInstanceID(" << sensor_id << ") error: " << SDL_GetError(); 94 | SDL_ClearError(); 95 | throw Napi::Error::New(env, message.str()); 96 | } 97 | 98 | Uint64 _timestamp; 99 | float data[3]; 100 | if (SDL_SensorGetDataWithTimestamp(sensor, &_timestamp, data, 3) == -1) { 101 | std::ostringstream message; 102 | message << "SDL_SensorGetDataWithTimestamp(" << sensor_id << ") error: " << SDL_GetError(); 103 | SDL_ClearError(); 104 | throw Napi::Error::New(env, message.str()); 105 | } 106 | Napi::Value timestamp = _timestamp != 0 107 | ? Napi::Number::New(env, _timestamp) 108 | : env.Null(); 109 | 110 | Napi::Object result = Napi::Object::New(env); 111 | result.Set("timestamp", timestamp); 112 | result.Set("x", data[0]); 113 | result.Set("y", data[1]); 114 | result.Set("z", data[2]); 115 | 116 | return result; 117 | } 118 | 119 | Napi::Value 120 | sensor::close (const Napi::CallbackInfo &info) 121 | { 122 | Napi::Env env = info.Env(); 123 | 124 | int sensor_id = info[0].As().Int32Value(); 125 | 126 | SDL_Sensor *sensor = SDL_SensorFromInstanceID(sensor_id); 127 | if (sensor == nullptr) { 128 | std::ostringstream message; 129 | message << "SDL_SensorFromInstanceID(" << sensor_id << ") error: " << SDL_GetError(); 130 | SDL_ClearError(); 131 | throw Napi::Error::New(env, message.str()); 132 | } 133 | 134 | SDL_SensorClose(sensor); 135 | 136 | return env.Undefined(); 137 | } 138 | -------------------------------------------------------------------------------- /src/native/sensor.h: -------------------------------------------------------------------------------- 1 | #ifndef _SENSOR_H_ 2 | #define _SENSOR_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace sensor { 10 | 11 | extern std::map types; 12 | extern std::map sides; 13 | 14 | Napi::Value getDevices(const Napi::CallbackInfo &info); 15 | Napi::Value open(const Napi::CallbackInfo &info); 16 | Napi::Value getData(const Napi::CallbackInfo &info); 17 | Napi::Value close(const Napi::CallbackInfo &info); 18 | 19 | }; // namespace sensor 20 | 21 | #endif // _SENSOR_H_ 22 | -------------------------------------------------------------------------------- /src/native/video.cpp: -------------------------------------------------------------------------------- 1 | #include "video.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | std::map video::orientations; 9 | std::map video::formats; 10 | 11 | 12 | Napi::Value 13 | video::getDisplays(const Napi::CallbackInfo &info) 14 | { 15 | Napi::Env env = info.Env(); 16 | 17 | int num_displays = SDL_GetNumVideoDisplays(); 18 | if (num_displays < 0) { 19 | std::ostringstream message; 20 | message << "SDL_GetNumVideoDisplays() error: " << SDL_GetError(); 21 | SDL_ClearError(); 22 | throw Napi::Error::New(env, message.str()); 23 | } 24 | 25 | Napi::Array displays = Napi::Array::New(env, num_displays); 26 | 27 | for (int i = 0; i < num_displays; i++) { 28 | const char *name = SDL_GetDisplayName(i); 29 | if(name == nullptr) { 30 | std::ostringstream message; 31 | message << "SDL_GetDisplayName(" << i << ") error: " << SDL_GetError(); 32 | SDL_ClearError(); 33 | throw Napi::Error::New(env, message.str()); 34 | } 35 | 36 | SDL_DisplayMode mode; 37 | if (SDL_GetCurrentDisplayMode(i, &mode) < 0) { 38 | std::ostringstream message; 39 | message << "SDL_GetCurrentDisplayMode(" << i << ") error: " << SDL_GetError(); 40 | SDL_ClearError(); 41 | throw Napi::Error::New(env, message.str()); 42 | } 43 | 44 | SDL_Rect rect; 45 | if(SDL_GetDisplayBounds(i, &rect) < 0) { 46 | std::ostringstream message; 47 | message << "SDL_GetDisplayBounds(" << i << ") error: " << SDL_GetError(); 48 | SDL_ClearError(); 49 | throw Napi::Error::New(env, message.str()); 50 | } 51 | 52 | Napi::Object geometry = Napi::Object::New(env); 53 | geometry.Set("x", rect.x); 54 | geometry.Set("y", rect.y); 55 | geometry.Set("width", rect.w); 56 | geometry.Set("height", rect.h); 57 | 58 | if(SDL_GetDisplayUsableBounds(i, &rect) < 0) { 59 | std::ostringstream message; 60 | message << "SDL_GetDisplayUsableBounds(" << i << ") error: " << SDL_GetError(); 61 | SDL_ClearError(); 62 | throw Napi::Error::New(env, message.str()); 63 | } 64 | 65 | Napi::Object usable = Napi::Object::New(env); 66 | usable.Set("x", rect.x); 67 | usable.Set("y", rect.y); 68 | usable.Set("width", rect.w); 69 | usable.Set("height", rect.h); 70 | 71 | Napi::Value dpi; 72 | float ddpi, hdpi, vdpi; 73 | if (SDL_GetDisplayDPI(i, &ddpi, &hdpi, &vdpi) < 0) { 74 | dpi = env.Null(); 75 | } else { 76 | Napi::Object dpi_obj = Napi::Object::New(env); 77 | dpi_obj.Set("diagonal", ddpi); 78 | dpi_obj.Set("horizontal", hdpi); 79 | dpi_obj.Set("vertical", vdpi); 80 | dpi = dpi_obj; 81 | } 82 | 83 | SDL_DisplayOrientation _orientation = SDL_GetDisplayOrientation(i); 84 | Napi::Value orientation = _orientation != SDL_ORIENTATION_UNKNOWN 85 | ? Napi::String::New(env, video::orientations[_orientation]) 86 | : env.Null(); 87 | 88 | Napi::Object display = Napi::Object::New(env); 89 | display.Set("_index", i); 90 | display.Set("name", name); 91 | display.Set("format", video::formats[(SDL_PixelFormatEnum) mode.format]); 92 | display.Set("frequency", mode.refresh_rate); 93 | display.Set("geometry", geometry); 94 | display.Set("usable", usable); 95 | display.Set("dpi", dpi); 96 | display.Set("orientation", orientation); 97 | 98 | displays.Set(i, display); 99 | } 100 | 101 | return displays; 102 | } 103 | -------------------------------------------------------------------------------- /src/native/video.h: -------------------------------------------------------------------------------- 1 | #ifndef _VIDEO_H_ 2 | #define _VIDEO_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace video { 9 | 10 | extern std::map orientations; 11 | extern std::map formats; 12 | 13 | Napi::Value getDisplays(const Napi::CallbackInfo &info); 14 | 15 | }; // namespace video 16 | 17 | #endif // _VIDEO_H_ 18 | -------------------------------------------------------------------------------- /src/native/window.h: -------------------------------------------------------------------------------- 1 | #ifndef _WINDOW_H_ 2 | #define _WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace window { 8 | 9 | Napi::Value create(const Napi::CallbackInfo &info); 10 | Napi::Value setTitle(const Napi::CallbackInfo &info); 11 | Napi::Value setPosition(const Napi::CallbackInfo &info); 12 | Napi::Value setSize(const Napi::CallbackInfo &info); 13 | Napi::Value setFullscreen(const Napi::CallbackInfo &info); 14 | Napi::Value setResizable(const Napi::CallbackInfo &info); 15 | Napi::Value setBorderless(const Napi::CallbackInfo &info); 16 | Napi::Value setAcceleratedAndVsync(const Napi::CallbackInfo &info); 17 | Napi::Value focus(const Napi::CallbackInfo &info); 18 | Napi::Value show(const Napi::CallbackInfo &info); 19 | Napi::Value hide(const Napi::CallbackInfo &info); 20 | Napi::Value maximize(const Napi::CallbackInfo &info); 21 | Napi::Value minimize(const Napi::CallbackInfo &info); 22 | Napi::Value restore(const Napi::CallbackInfo &info); 23 | Napi::Value render(const Napi::CallbackInfo &info); 24 | Napi::Value setIcon(const Napi::CallbackInfo &info); 25 | Napi::Value flash(const Napi::CallbackInfo &info); 26 | Napi::Value destroy(const Napi::CallbackInfo &info); 27 | 28 | }; // namespace window 29 | 30 | #endif // _WINDOW_H_ 31 | -------------------------------------------------------------------------------- /src/types/helpers.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export namespace SdlHelpers { 3 | 4 | export namespace Audio { 5 | 6 | export type Format 7 | = 's8' 8 | | 'u8' 9 | | 's16lsb' 10 | | 's16msb' 11 | | 's16sys' 12 | | 's16' 13 | | 'u16lsb' 14 | | 'u16msb' 15 | | 'u16sys' 16 | | 'u16' 17 | | 's32lsb' 18 | | 's32msb' 19 | | 's32sys' 20 | | 's32' 21 | | 'f32lsb' 22 | | 'f32msb' 23 | | 'f32sys' 24 | | 'f32' 25 | 26 | interface Module { 27 | bytesPerSample (format: Format): number 28 | minSampleValue (format: Format): number 29 | maxSampleValue (format: Format): number 30 | zeroSampleValue (format: Format): number 31 | readSample (format: Format, buffer: Buffer, offset?: number): number 32 | writeSample (format: Format, buffer: Buffer, value: number, offset?: number): number 33 | } 34 | 35 | } 36 | 37 | } 38 | 39 | export const audio: SdlHelpers.Audio.Module 40 | -------------------------------------------------------------------------------- /tests/audio.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::audio", async (t) => { 5 | t.timeout(3e3) 6 | 7 | t.equal(sdl.audio.bytesPerSample('f32lsb'), 4) 8 | t.equal(sdl.audio.minSampleValue('f32lsb'), -1) 9 | t.equal(sdl.audio.maxSampleValue('f32lsb'), 1) 10 | t.equal(sdl.audio.zeroSampleValue('f32lsb'), 0) 11 | 12 | 13 | t.ok(Array.isArray(sdl.audio.devices)) 14 | 15 | for (const device of sdl.audio.devices) { 16 | t.ok([ 'playback', 'recording' ].includes(device.type)) 17 | t.equal(typeof device.name, 'string') 18 | t.ok(device.name.length > 0) 19 | } 20 | 21 | const playbackDevices = sdl.audio.devices.filter(({ type }) => type === 'playback') 22 | const recordingDevices = sdl.audio.devices.filter(({ type }) => type === 'recording') 23 | 24 | if (playbackDevices.length === 0) { 25 | console.warn("NO AUDIO PLAYBACK FOUND") 26 | } 27 | else { 28 | const device = { type: 'playback' } 29 | 30 | const instance1 = sdl.audio.openDevice(device) 31 | const instance2 = sdl.audio.openDevice(playbackDevices[0], { 32 | channels: 2, 33 | frequency: 44100, 34 | format: 'u16', 35 | buffered: 1024, 36 | }) 37 | 38 | t.equal(typeof instance1.id, 'number') 39 | t.equal(typeof instance2.id, 'number') 40 | t.notEqual(instance1.id, instance2.id) 41 | 42 | t.equal(instance1.device, device) 43 | t.equal(playbackDevices[0], instance2.device) 44 | 45 | t.equal(instance1.channels, 1) 46 | t.equal(instance2.channels, 2) 47 | 48 | t.equal(instance1.frequency, 48e3) 49 | t.equal(instance2.frequency, 44100) 50 | 51 | t.equal(instance1.format, 'f32') 52 | t.equal(instance2.format, 'u16') 53 | 54 | t.equal(instance1.bytesPerSample, 4) 55 | t.equal(instance2.bytesPerSample, 2) 56 | 57 | t.equal(instance1.minSampleValue, -1) 58 | t.equal(instance2.minSampleValue, 0) 59 | 60 | t.equal(instance1.maxSampleValue, 1) 61 | t.equal(instance2.maxSampleValue, 65535) 62 | 63 | t.equal(instance1.zeroSampleValue, 0) 64 | t.equal(instance2.zeroSampleValue, 32767) 65 | 66 | t.equal(instance1.buffered, 4096) 67 | t.equal(instance2.buffered, 1024) 68 | 69 | t.equal(instance1.playing, false) 70 | t.equal(instance2.playing, false) 71 | 72 | instance1.play() 73 | t.equal(instance1.playing, true) 74 | t.equal(instance2.playing, false) 75 | 76 | instance2.play(true) 77 | t.equal(instance1.playing, true) 78 | t.equal(instance2.playing, true) 79 | 80 | instance1.pause() 81 | t.equal(instance1.playing, false) 82 | t.equal(instance2.playing, true) 83 | 84 | instance2.play(false) 85 | t.equal(instance1.playing, false) 86 | t.equal(instance2.playing, false) 87 | 88 | t.equal(instance1.queued, 0) 89 | t.equal(instance2.queued, 0) 90 | 91 | const buffer = Buffer.alloc(256) 92 | 93 | instance1.enqueue(buffer) 94 | t.equal(instance1.queued, 256) 95 | t.equal(instance2.queued, 0) 96 | 97 | instance2.enqueue(buffer) 98 | t.equal(instance1.queued, 256) 99 | t.equal(instance2.queued, 256) 100 | 101 | instance1.clearQueue() 102 | t.equal(instance1.queued, 0) 103 | t.equal(instance2.queued, 256) 104 | 105 | instance2.clearQueue() 106 | t.equal(instance1.queued, 0) 107 | t.equal(instance2.queued, 0) 108 | 109 | instance1.close() 110 | instance2.close() 111 | } 112 | 113 | if (recordingDevices.length === 0) { 114 | console.warn("NO AUDIO RECORDING FOUND") 115 | } 116 | else { 117 | const device = { type: 'recording' } 118 | 119 | const instance1 = sdl.audio.openDevice(device) 120 | const instance2 = sdl.audio.openDevice(recordingDevices[0], { 121 | channels: 2, 122 | frequency: 44100, 123 | format: 'u16', 124 | buffered: 1024, 125 | }) 126 | 127 | t.equal(typeof instance1.id, 'number') 128 | t.equal(typeof instance2.id, 'number') 129 | t.notEqual(instance1.id, instance2.id) 130 | 131 | t.equal(instance1.device, device) 132 | t.equal(recordingDevices[0], instance2.device) 133 | 134 | t.equal(instance1.channels, 1) 135 | t.equal(instance2.channels, 2) 136 | 137 | t.equal(instance1.frequency, 48e3) 138 | t.equal(instance2.frequency, 44100) 139 | 140 | t.equal(instance1.format, 'f32') 141 | t.equal(instance2.format, 'u16') 142 | 143 | t.equal(instance1.bytesPerSample, 4) 144 | t.equal(instance2.bytesPerSample, 2) 145 | 146 | t.equal(instance1.minSampleValue, -1) 147 | t.equal(instance2.minSampleValue, 0) 148 | 149 | t.equal(instance1.maxSampleValue, 1) 150 | t.equal(instance2.maxSampleValue, 65535) 151 | 152 | t.equal(instance1.zeroSampleValue, 0) 153 | t.equal(instance2.zeroSampleValue, 32767) 154 | 155 | t.equal(instance1.buffered, 4096) 156 | t.equal(instance2.buffered, 1024) 157 | 158 | t.equal(instance1.playing, false) 159 | t.equal(instance2.playing, false) 160 | 161 | t.equal(instance1.queued, 0) 162 | t.equal(instance2.queued, 0) 163 | 164 | instance1.play() 165 | t.equal(instance1.playing, true) 166 | t.equal(instance2.playing, false) 167 | 168 | instance2.play(true) 169 | t.equal(instance1.playing, true) 170 | t.equal(instance2.playing, true) 171 | 172 | await new Promise((resolve) => { setTimeout(resolve, 1e3) }) 173 | 174 | t.ok(instance1.queued > 0) 175 | t.ok(instance2.queued > 0) 176 | 177 | const buffer = Buffer.alloc(256) 178 | 179 | const num1 = instance1.dequeue(buffer) 180 | t.ok(0 < num1 && num1 <= 256) 181 | 182 | const num2 = instance2.dequeue(buffer) 183 | t.ok(0 < num2 && num2 <= 256) 184 | 185 | instance1.pause() 186 | t.equal(instance1.playing, false) 187 | t.equal(instance2.playing, true) 188 | 189 | instance2.play(false) 190 | t.equal(instance1.playing, false) 191 | t.equal(instance2.playing, false) 192 | 193 | instance1.clearQueue() 194 | t.equal(instance1.queued, 0) 195 | t.ok(instance2.queued > 0) 196 | 197 | instance2.clearQueue() 198 | t.equal(instance1.queued, 0) 199 | t.equal(instance2.queued, 0) 200 | 201 | instance1.close() 202 | instance2.close() 203 | } 204 | }) 205 | -------------------------------------------------------------------------------- /tests/clipboard.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::clipboard", (t) => { 5 | if (sdl.clipboard.text !== null) { 6 | t.equal(typeof sdl.clipboard.text, 'string') 7 | } 8 | 9 | const random1 = `${Math.random()}`.slice(2) 10 | sdl.clipboard.setText(random1) 11 | t.equal(sdl.clipboard.text, random1) 12 | 13 | const random2 = `${Math.random()}`.slice(2) 14 | sdl.clipboard.setText(random2) 15 | t.equal(sdl.clipboard.text, random2) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/controller.test.mjs: -------------------------------------------------------------------------------- 1 | import E from '@kmamal/evdev' 2 | 3 | const virtualJoystick = E.uinput.createDevice({ 4 | name: 'Emulated Joystick', 5 | events: [ 6 | { 7 | type: E.EV_KEY, 8 | codes: [ E.BTN_A, E.BTN_B, E.BTN_X, E.BTN_Y ], 9 | }, 10 | { 11 | type: E.EV_ABS, 12 | codes: [ 13 | { code: E.ABS_X, min: -100, max: +100 }, 14 | { code: E.ABS_Y, min: -100, max: +100 }, 15 | ], 16 | }, 17 | ], 18 | }) 19 | 20 | 21 | await new Promise((resolve) => { setTimeout(resolve, 3e3) }) 22 | 23 | 24 | import T from '@kmamal/testing' 25 | import sdl from '../src/javascript/index.js' 26 | 27 | T.test("sdl::controller", (t) => { 28 | t.ok(Array.isArray(sdl.controller.devices)) 29 | 30 | t.equal(sdl.controller.devices.length, 1) 31 | 32 | const device = sdl.controller.devices[0] 33 | 34 | t.equal(typeof device.id, 'number') 35 | t.equal(device.name, 'Emulated Joystick') 36 | t.equal(typeof device.path, 'string') 37 | t.equal(typeof device.guid, 'string') 38 | t.equal(device.guid.length, 32) 39 | t.equal(device.vendor, null) 40 | t.equal(device.product, null) 41 | t.equal(device.version, null) 42 | t.equal(typeof device.player, 'number') 43 | 44 | t.equal(typeof device.mapping, 'string') 45 | t.equal(device.mapping.slice(0, 32), device.guid) 46 | t.equal(device.mapping.slice(33, 33 + device.name.length), device.name) 47 | 48 | const instance = sdl.controller.openDevice(device) 49 | 50 | t.equal(instance.device, device) 51 | t.equal(instance.firmwareVersion, null) 52 | t.equal(instance.serialNumber, null) 53 | t.equal(instance.steamHandle, null) 54 | t.equal(instance.hasLed, false) 55 | t.equal(instance.hasRumble, false) 56 | t.equal(instance.hasRumbleTriggers, false) 57 | 58 | t.equal(Object.keys(instance.axes), [ 59 | 'leftStickX', 60 | 'leftStickY', 61 | 'rightStickX', 62 | 'rightStickY', 63 | 'leftTrigger', 64 | 'rightTrigger', 65 | ]) 66 | 67 | t.equal(instance.buttons, { 68 | dpadLeft: false, 69 | dpadRight: false, 70 | dpadUp: false, 71 | dpadDown: false, 72 | a: false, 73 | b: false, 74 | x: false, 75 | y: false, 76 | guide: false, 77 | back: false, 78 | start: false, 79 | leftStick: false, 80 | rightStick: false, 81 | leftShoulder: false, 82 | rightShoulder: false, 83 | paddle1: false, 84 | paddle2: false, 85 | paddle3: false, 86 | paddle4: false, 87 | }) 88 | 89 | const newMapping = device.mapping.replace(device.name, 'foobar') 90 | sdl.controller.addMappings([ newMapping ]) 91 | t.equal(instance.device, device) 92 | t.equal(instance.device.name, 'foobar') 93 | 94 | t.equal(instance.closed, false) 95 | instance.close() 96 | t.equal(instance.closed, true) 97 | 98 | virtualJoystick.destroy() 99 | }) 100 | -------------------------------------------------------------------------------- /tests/info.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::info", (t) => { 5 | t.equal(typeof sdl.info.version.compile.major, 'number') 6 | t.equal(typeof sdl.info.version.compile.minor, 'number') 7 | t.equal(typeof sdl.info.version.compile.patch, 'number') 8 | 9 | t.equal(typeof sdl.info.version.runtime.major, 'number') 10 | t.equal(typeof sdl.info.version.runtime.minor, 'number') 11 | t.equal(typeof sdl.info.version.runtime.patch, 'number') 12 | 13 | t.ok([ 'Linux', 'Mac OS X', 'Windows' ].includes(sdl.info.platform)) 14 | 15 | t.ok(Array.isArray(sdl.info.drivers.video.all)) 16 | for (const driver of sdl.info.drivers.video.all) { 17 | t.equal(typeof driver, 'string') 18 | t.ok(driver.length > 0) 19 | } 20 | t.equal(typeof sdl.info.drivers.video.current, 'string') 21 | t.ok(sdl.info.drivers.video.all.includes(sdl.info.drivers.video.current)) 22 | 23 | t.ok(Array.isArray(sdl.info.drivers.audio.all)) 24 | for (const driver of sdl.info.drivers.audio.all) { 25 | t.equal(typeof driver, 'string') 26 | t.ok(driver.length > 0) 27 | } 28 | t.equal(typeof sdl.info.drivers.audio.current, 'string') 29 | t.ok(sdl.info.drivers.audio.all.includes(sdl.info.drivers.audio.current)) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/joystick.test.mjs: -------------------------------------------------------------------------------- 1 | import E from '@kmamal/evdev' 2 | 3 | const virtualJoystick = E.uinput.createDevice({ 4 | name: 'Emulated Joystick', 5 | events: [ 6 | { 7 | type: E.EV_KEY, 8 | codes: [ E.BTN_A, E.BTN_B, E.BTN_X, E.BTN_Y ], 9 | }, 10 | { 11 | type: E.EV_ABS, 12 | codes: [ 13 | { code: E.ABS_X, min: -100, max: +100 }, 14 | { code: E.ABS_Y, min: -100, max: +100 }, 15 | ], 16 | }, 17 | ], 18 | }) 19 | 20 | 21 | await new Promise((resolve) => { setTimeout(resolve, 3e3) }) 22 | 23 | 24 | import T from '@kmamal/testing' 25 | import sdl from '../src/javascript/index.js' 26 | 27 | T.test("sdl::joystick", (t) => { 28 | t.ok(Array.isArray(sdl.joystick.devices)) 29 | 30 | t.equal(sdl.joystick.devices.length, 1) 31 | 32 | const device = sdl.joystick.devices[0] 33 | 34 | t.equal(typeof device.id, 'number') 35 | t.equal(device.type, 'gamecontroller') 36 | t.equal(device.name, 'Emulated Joystick') 37 | t.equal(typeof device.path, 'string') 38 | t.equal(typeof device.guid, 'string') 39 | t.equal(device.guid.length, 32) 40 | t.equal(device.vendor, null) 41 | t.equal(device.product, null) 42 | t.equal(device.version, null) 43 | t.equal(typeof device.player, 'number') 44 | 45 | const instance = sdl.joystick.openDevice(device) 46 | 47 | t.equal(instance.device, device) 48 | t.equal(instance.firmwareVersion, null) 49 | t.equal(instance.serialNumber, null) 50 | t.equal(instance.hasLed, false) 51 | t.equal(instance.hasRumble, false) 52 | t.equal(instance.hasRumbleTriggers, false) 53 | 54 | t.ok(Array.isArray(instance.axes)) 55 | t.equal(instance.axes.length, 2) 56 | for (const axis of instance.axes) { 57 | t.equal(typeof axis, 'number') 58 | } 59 | 60 | t.ok(Array.isArray(instance.buttons)) 61 | t.equal(instance.buttons.length, 2) 62 | for (const button of instance.buttons) { 63 | t.equal(typeof button, 'boolean') 64 | } 65 | 66 | t.ok(Array.isArray(instance.balls)) 67 | t.equal(instance.balls.length, 0) 68 | 69 | t.ok(Array.isArray(instance.hats)) 70 | t.equal(instance.hats.length, 0) 71 | 72 | t.equal(instance.closed, false) 73 | instance.close() 74 | t.equal(instance.closed, true) 75 | 76 | virtualJoystick.destroy() 77 | }) 78 | -------------------------------------------------------------------------------- /tests/keyboard.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::keyboard", (t) => { 5 | const scancodes1 = Object.entries(sdl.keyboard.SCANCODE).map(([ , v ]) => v) 6 | t.ok(scancodes1.length > 200) 7 | const scancodeSet1 = new Set(scancodes1) 8 | t.equal(scancodeSet1.size, scancodes1.length) 9 | 10 | const keys = [] 11 | for (const code of scancodes1) { 12 | t.equal(typeof code, 'number') 13 | t.ok(code > 0) 14 | 15 | const key = sdl.keyboard.getKey(code) 16 | if (key === null) { 17 | keys.push(null) 18 | continue 19 | } 20 | 21 | t.equal(typeof key, 'string') 22 | t.ok(key.length >= 1) 23 | keys.push(key) 24 | } 25 | 26 | t.ok(new Set(keys).size > 100) 27 | 28 | const scancodes2 = new Array(scancodes1.length) 29 | for (let i = 0; i < scancodes1.length; i++) { 30 | const key = keys[i] 31 | if (key === null) { 32 | scancodes2[i] = null 33 | continue 34 | } 35 | 36 | const code = sdl.keyboard.getScancode(key) 37 | scancodes2[i] = code 38 | 39 | if (code === null) { continue } 40 | t.equal(typeof code, 'number') 41 | t.ok(code > 0) 42 | } 43 | const scancodeSet2 = new Set(scancodes2) 44 | t.ok(scancodeSet1.size - scancodeSet2.size < 80) 45 | 46 | 47 | // // 48 | // const entries = Object.entries(sdl.keyboard.SCANCODE) 49 | // for (let i = 0; i < entries.length; i++) { 50 | // console.log(entries[i], keys[i], scancodes2[i]) 51 | // } 52 | // // 53 | 54 | 55 | const state = sdl.keyboard.getState() 56 | 57 | let numPressed = 0 58 | for (const isPressed of state) { 59 | t.equal(typeof isPressed, 'boolean') 60 | if (isPressed) { numPressed++ } 61 | } 62 | t.ok(numPressed <= 3) 63 | 64 | for (const scancode of Object.values(sdl.keyboard.SCANCODE)) { 65 | t.equal(typeof state[scancode], 'boolean') 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /tests/mouse.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::mouse", async (t) => { 5 | const buttons = Object.entries(sdl.mouse.BUTTON).map(([ , v ]) => v) 6 | t.ok(buttons.length > 1) 7 | const buttonSet = new Set(buttons) 8 | t.equal(buttonSet.size, buttons.length) 9 | 10 | let numPressed = 0 11 | for (const button of buttons) { 12 | const isPressed = sdl.mouse.getButton(button) 13 | t.equal(typeof isPressed, 'boolean') 14 | if (isPressed) { numPressed++ } 15 | } 16 | t.ok(numPressed <= 1) 17 | 18 | 19 | const { x, y } = sdl.mouse.position 20 | t.equal(typeof x, 'number') 21 | t.equal(typeof y, 'number') 22 | 23 | sdl.mouse.setPosition(200, 100) 24 | const { x: xx, y: yy } = sdl.mouse.position 25 | t.equal(xx, 200) 26 | t.equal(yy, 100) 27 | sdl.mouse.setPosition(x, y) 28 | 29 | 30 | sdl.mouse.setCursor('wait') 31 | sdl.mouse.resetCursor() 32 | 33 | 34 | sdl.mouse.hideCursor() 35 | sdl.mouse.showCursor() 36 | sdl.mouse.showCursor(false) 37 | sdl.mouse.showCursor(true) 38 | sdl.mouse.redrawCursor() 39 | 40 | 41 | const window = sdl.video.createWindow() 42 | window.focus() 43 | 44 | await new Promise((resolve) => { setTimeout(resolve, 0) }) 45 | 46 | sdl.mouse.capture() 47 | sdl.mouse.uncapture() 48 | sdl.mouse.capture(true) 49 | sdl.mouse.capture(false) 50 | 51 | window.destroy() 52 | }) 53 | -------------------------------------------------------------------------------- /tests/power.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::power", (t) => { 5 | t.ok([ 'noBattery', 'battery', 'charging', 'charged', null ].includes(sdl.power.info.state)) 6 | 7 | if (sdl.power.info.seconds !== null) { 8 | t.equal(typeof sdl.power.info.seconds, 'number') 9 | t.ok(sdl.power.info.seconds > 0) 10 | } 11 | 12 | if (sdl.power.info.percent !== null) { 13 | t.equal(typeof sdl.power.info.percent, 'number') 14 | t.ok(0 <= sdl.power.info.percent <= 100) 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /tests/sensor.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | // const E = require('@kmamal/evdev') 4 | 5 | test("sdl::sensor", (t) => { 6 | t.equal(sdl.sensor.STANDARD_GRAVITY, 9.80665) 7 | 8 | 9 | t.ok(Array.isArray(sdl.sensor.devices)) 10 | 11 | if (sdl.sensor.devices.length !== 0) { 12 | throw new Error("Please disconnect all sensors before running tests") 13 | } 14 | 15 | // const sensor = E.uinput.createDevice({ 16 | // name: "Emulated Accelerometer", 17 | // props: [ E.INPUT_PROP_ACCELEROMETER ], 18 | // events: [ 19 | // { type: E.EV_ABS, 20 | // codes: [ 21 | // { code: E.ABS_X, min: -100, max: +100 }, 22 | // { code: E.ABS_Y, min: -100, max: +100 }, 23 | // { code: E.ABS_Z, min: -100, max: +100 }, 24 | // ] }, 25 | // ], 26 | // }) 27 | 28 | // t.equal(sdl.sensor.devices.length, 1) 29 | 30 | // 31 | 32 | console.warn("SENSOR TESTS NOT IMPLEMENTED YET") 33 | }) 34 | -------------------------------------------------------------------------------- /tests/video.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::video", (t) => { 5 | t.ok(Array.isArray(sdl.video.displays)) 6 | 7 | if (sdl.video.displays.length === 0) { 8 | console.warn("NO DISPLAYS FOUND") 9 | return 10 | } 11 | 12 | for (const display of sdl.video.displays) { 13 | t.equal(typeof display.name, 'string') 14 | t.ok(display.name.length > 0) 15 | 16 | t.equal(typeof display.format, 'string') 17 | t.ok(display.format.length > 0) 18 | 19 | t.equal(typeof display.frequency, 'number') 20 | 21 | t.equal(typeof display.geometry.x, 'number') 22 | t.equal(typeof display.geometry.y, 'number') 23 | t.equal(typeof display.geometry.width, 'number') 24 | t.equal(typeof display.geometry.height, 'number') 25 | 26 | t.equal(typeof display.usable.x, 'number') 27 | t.equal(typeof display.usable.y, 'number') 28 | t.equal(typeof display.usable.width, 'number') 29 | t.equal(typeof display.usable.height, 'number') 30 | 31 | if (display.dpi !== null) { 32 | t.equal(typeof display.dpi.horizontal, 'number') 33 | t.equal(typeof display.dpi.vertical, 'number') 34 | t.equal(typeof display.dpi.diagonal, 'number') 35 | } 36 | 37 | if (display.orientation !== null) { 38 | t.ok(typeof display.orientation, 'string') 39 | } 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /tests/window.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('@kmamal/testing') 2 | const sdl = require('../src/javascript/index.js') 3 | 4 | test("sdl::window", (t) => { 5 | t.ok(Array.isArray(sdl.video.windows)) 6 | t.equal(sdl.video.windows.length, 0) 7 | t.equal(sdl.video.focused, null) 8 | t.equal(sdl.video.hovered, null) 9 | 10 | t.equal(typeof sdl.video.createWindow, 'function') 11 | 12 | const window1 = sdl.video.createWindow() 13 | 14 | t.equal(sdl.video.windows.length, 1) 15 | t.equal(sdl.video.windows[0], window1) 16 | 17 | const window2 = sdl.video.createWindow({ 18 | title: 'test2', 19 | x: 12, 20 | y: 23, 21 | width: 67, 22 | height: 78, 23 | visible: false, 24 | resizable: true, 25 | alwaysOnTop: true, 26 | accelerated: false, 27 | vsync: false, 28 | }) 29 | 30 | t.equal(sdl.video.windows.length, 2) 31 | t.equal(sdl.video.windows[0], window1) 32 | t.equal(sdl.video.windows[1], window2) 33 | 34 | t.equal(typeof window1.id, 'number') 35 | t.equal(typeof window2.id, 'number') 36 | t.notEqual(window1.id, window2.id) 37 | 38 | t.equal(window1.title, '') 39 | t.equal(window2.title, 'test2') 40 | window1.setTitle('test1') 41 | t.equal(window1.title, 'test1') 42 | 43 | t.equal(typeof window1.x, 'number') 44 | t.equal(typeof window1.y, 'number') 45 | t.equal(window2.x, 12) 46 | t.equal(window2.y, 23) 47 | window1.setPosition(34, 56) 48 | t.equal(window1.x, 34) 49 | t.equal(window1.y, 56) 50 | 51 | t.equal(typeof window1.width, 'number') 52 | t.equal(typeof window1.height, 'number') 53 | t.equal(window2.width, 67) 54 | t.equal(window2.height, 78) 55 | window1.setSize(89, 90) 56 | t.equal(window1.width, 89) 57 | t.equal(window1.height, 90) 58 | 59 | t.equal(typeof window1.pixelWidth, 'number') 60 | t.equal(typeof window1.pixelHeight, 'number') 61 | 62 | t.equal(window1.visible, true) 63 | t.equal(window2.visible, false) 64 | window1.hide() 65 | t.equal(window1.visible, false) 66 | window1.show() 67 | t.equal(window1.visible, true) 68 | window1.show(false) 69 | t.equal(window1.visible, false) 70 | window1.show(true) 71 | t.equal(window1.visible, true) 72 | 73 | t.equal(window1.fullscreen, false) 74 | { 75 | const window3 = sdl.video.createWindow({ fullscreen: true }) 76 | t.equal(window3.fullscreen, true) 77 | window3.destroy() 78 | } 79 | window1.setFullscreen(true) 80 | t.equal(window1.fullscreen, true) 81 | window1.setFullscreen(false) 82 | t.equal(window1.fullscreen, false) 83 | 84 | t.equal(window1.resizable, false) 85 | t.equal(window2.resizable, true) 86 | window1.setResizable(true) 87 | t.equal(window1.resizable, true) 88 | window1.setResizable(false) 89 | t.equal(window1.resizable, false) 90 | 91 | t.equal(window1.borderless, false) 92 | { 93 | const window3 = sdl.video.createWindow({ borderless: true }) 94 | t.equal(window3.borderless, true) 95 | window3.destroy() 96 | } 97 | window1.setBorderless(true) 98 | t.equal(window1.borderless, true) 99 | window1.setBorderless(false) 100 | t.equal(window1.borderless, false) 101 | 102 | t.equal(window1.alwaysOnTop, false) 103 | t.equal(window2.alwaysOnTop, true) 104 | 105 | t.equal(window1.accelerated, true) 106 | t.equal(window2.accelerated, false) 107 | window1.setAccelerated(false) 108 | t.equal(window1.accelerated, false) 109 | window1.setAccelerated(true) 110 | t.equal(window1.accelerated, true) 111 | 112 | t.equal(window1.vsync, true) 113 | // t.equal(window2.vsync, false) // TODO: set automatically to true 114 | window1.setVsync(false) 115 | t.equal(window1.vsync, false) 116 | window1.setVsync(true) 117 | t.equal(window1.vsync, true) 118 | 119 | t.equal(window1.opengl, false) 120 | t.equal(window1.webgpu, false) 121 | t.ok(Buffer.isBuffer(window1.native.handle)) 122 | 123 | t.equal(window1.maximized, false) 124 | t.equal(window1.minimized, false) 125 | window1.maximize() 126 | t.equal(window1.maximized, true) 127 | t.equal(window1.minimized, false) 128 | window1.minimize() 129 | t.equal(window1.maximized, false) 130 | t.equal(window1.minimized, true) 131 | window1.restore() 132 | t.equal(window1.maximized, false) 133 | t.equal(window1.minimized, false) 134 | 135 | t.equal(window1.focused, false) 136 | t.equal(window2.focused, false) 137 | window1.focus() 138 | t.equal(window1.focused, true) 139 | t.equal(window2.focused, false) 140 | t.equal(sdl.video.focused, window1) 141 | window2.focus() 142 | t.equal(window1.focused, false) 143 | t.equal(window2.focused, true) 144 | t.equal(sdl.video.focused, window2) 145 | window1.focus() 146 | 147 | t.equal(typeof window1.hovered, 'boolean') 148 | 149 | t.equal(typeof window1.skipTaskbar, 'boolean') 150 | t.equal(typeof window1.popupMenu, 'boolean') 151 | t.equal(typeof window1.tooltip, 'boolean') 152 | t.equal(typeof window1.utility, 'boolean') 153 | 154 | t.equal(typeof window1.render, 'function') 155 | 156 | t.equal(typeof window1.setIcon, 'function') 157 | 158 | t.equal(typeof window1.flash, 'function') 159 | t.equal(typeof window1.stopFlashing, 'function') 160 | 161 | t.equal(window1.display, sdl.video.displays[0]) 162 | t.equal(window2.display, sdl.video.displays[0]) 163 | 164 | t.equal(window1.destroyed, false) 165 | t.equal(window2.destroyed, false) 166 | window1.destroy() 167 | t.equal(window1.destroyed, true) 168 | t.equal(window2.destroyed, false) 169 | 170 | t.equal(sdl.video.windows.length, 1) 171 | t.equal(sdl.video.windows[0], window2) 172 | t.equal(window2.focused, false) 173 | t.equal(sdl.video.focused, null) 174 | 175 | window2.destroy() 176 | t.equal(window1.destroyed, true) 177 | t.equal(window2.destroyed, true) 178 | 179 | t.equal(sdl.video.windows.length, 0) 180 | }) 181 | --------------------------------------------------------------------------------